From 639401ea0e2e9a1a50d8177479f282e331a15ae4 Mon Sep 17 00:00:00 2001 From: eeng5 Date: Mon, 19 Aug 2019 10:28:52 -0400 Subject: ; --- src/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/server') diff --git a/src/server/index.ts b/src/server/index.ts index eae018f13..261d1abdd 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -436,7 +436,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(1); + let viewport = page.getViewport(1 as any); let canvasAndContext = factory.create(viewport.width, viewport.height); let renderContext = { canvasContext: canvasAndContext.context, -- cgit v1.2.3-70-g09d2 From 77e08a4362ba8fab4cab361fcb472702c97edf15 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 1 Sep 2019 13:05:54 -0400 Subject: initial commit --- package.json | 3 + src/client/views/MainView.tsx | 4 +- src/server/apis/google/GoogleApiServerUtils.ts | 13 +- src/server/apis/google/GooglePhotosUtils.ts | 12 + src/server/authentication/config/passport.ts | 18 +- src/server/credentials/auth.json | 12 + .../credentials/google_docs_credentials.json | 12 +- src/server/credentials/google_docs_token.json | 2 +- .../credentials/google_photos_credentials.ts | 35 ++ src/server/index.ts | 27 + src/typings/index.d.ts | 632 +++++++++++---------- 11 files changed, 447 insertions(+), 323 deletions(-) create mode 100644 src/server/apis/google/GooglePhotosUtils.ts create mode 100644 src/server/credentials/auth.json create mode 100644 src/server/credentials/google_photos_credentials.ts (limited to 'src/server') diff --git a/package.json b/package.json index f98224469..f0f2b467e 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@types/node": "^10.12.30", "@types/nodemailer": "^4.6.6", "@types/passport": "^1.0.0", + "@types/passport-google-oauth20": "^2.0.2", "@types/passport-local": "^1.0.33", "@types/pdfjs-dist": "^2.0.0", "@types/prosemirror-commands": "^1.0.1", @@ -141,6 +142,7 @@ "golden-layout": "^1.5.9", "google-auth-library": "^4.2.4", "googleapis": "^40.0.0", + "googlephotos": "^0.2.1", "howler": "^2.1.2", "html-to-image": "^0.1.0", "i": "^0.3.6", @@ -166,6 +168,7 @@ "npm": "^6.10.3", "p-limit": "^2.2.0", "passport": "^0.4.0", + "passport-google-oauth20": "^2.0.0", "passport-local": "^1.0.0", "pdfjs-dist": "^2.0.943", "probe-image-size": "^4.0.0", diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ab87f0c7b..df0718072 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 } 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'; @@ -128,6 +128,8 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + PostToServer('/googleDocs/Photos/Test', {}); + reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; let recent = CurrentUserUtils.UserDocument.recentlyClosed; diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 8785cd974..2fb44d9a2 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -5,6 +5,7 @@ import { OAuth2Client } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; +import Photos = require("googlephotos"); /** * Server side authentication for Google Api queries. @@ -20,16 +21,18 @@ export namespace GoogleApiServerUtils { 'presentations.readonly', 'drive', 'drive.file', + 'photoslibrary', + 'photoslibrary.sharing' ]; export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); export enum Service { Documents = "Documents", - Slides = "Slides" + Slides = "Slides", + Photos = "Photos" } - export interface CredentialPaths { credentials: string; token: string; @@ -50,7 +53,7 @@ export namespace GoogleApiServerUtils { reject(err); return console.log('Error loading client secret file:', err); } - return authorize(parseBuffer(credentials), paths.token).then(auth => { + return authorize(parseBuffer(credentials), paths.token).then(async auth => { let routed: Opt; let parameters: EndpointParameters = { auth, version: "v1" }; switch (sector) { @@ -60,6 +63,10 @@ export namespace GoogleApiServerUtils { case Service.Slides: routed = google.slides(parameters).presentations; break; + case Service.Photos: + const photos = new Photos(auth); + let response = await photos.albums.list(); + console.log("WE GOT SOMETHING!", response); } resolve(routed); }); diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts new file mode 100644 index 000000000..c33ad2dd9 --- /dev/null +++ b/src/server/apis/google/GooglePhotosUtils.ts @@ -0,0 +1,12 @@ +import request = require('request-promise'); +const key = require("../../credentials/auth.json"); + +export const PhotosLibraryQuery = async (authToken: any, parameters: any) => { + let options = { + headers: { 'Content-Type': 'application/json' }, + json: parameters, + auth: { 'bearer': authToken }, + }; + const result = await request.post(config.apiEndpoint + '/v1/mediaItems:search', options); + return result; +}; \ No newline at end of file diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index d42741410..97ded8785 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -1,12 +1,14 @@ 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'; +import * as GoogleOAuth from "passport-google-oauth20"; +const config = require("../../credentials/google_photos_credentials"); const LocalStrategy = passportLocal.Strategy; +const GoogleOAuthStrategy = GoogleOAuth.Strategy; passport.serializeUser((user, done) => { done(undefined, user.id); @@ -32,6 +34,18 @@ passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true }); })); + +passport.use(new GoogleOAuthStrategy( + { + clientID: config.oAuthClientID, + clientSecret: config.oAuthclientSecret, + callbackURL: config.oAuthCallbackUrl, + // Set the correct profile URL that does not require any additional APIs + userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' + }, + (token, refreshToken, profile, done) => done(undefined, { profile, token }) +)); + export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => { if (req.isAuthenticated()) { return next(); diff --git a/src/server/credentials/auth.json b/src/server/credentials/auth.json new file mode 100644 index 000000000..557eca4b6 --- /dev/null +++ b/src/server/credentials/auth.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "brown-dash", + "private_key_id": "ddf0473a9ac56956b5818e04a7ee406a64d5b0a6", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCueRfxic2oL9nr\nnWSLgl7XR/BKikm4p2sib6szaoTO+q6itcJgt2TDleK/7Y4KW/KhvCfhWVet0Hz0\nIDyg4N/gc2yxuDA6/m8DPWU9kDj8VFR7LVFawOKgo1WbgLcC0Qu8qHzAffrlg8si\nhj3vGuoS/YDn/mz0krwFmCfIx+S0lJ9a7FUjJL5C+CIwAEEYiU7xnTW7pVVNXAm/\n/YKD17ToAjREOtlfVVYO7tZ7V5BiW0I0jJvxw+t1pgrZZe7WPBSBJg9KKGIl+mRi\ndtUMR9Hyt3nMKNZIrSm0OkAz82HxfcapRdSB3wkVjoyW63YaTVHKoBOqRElfMtoM\nqu8wbhhNAgMBAAECggEAA41wJ8kg8J8peQMZ/b7gZvzuPy+h0M/J3j2MrG3rY9qA\nrUv1oqoBSXvhuNDhtEN12oYIqtg6m+L+Sas8CMuOC2rWPafM2u/80IGoGtDhtCjp\nv8inBX8ew4YSiL7IxdbTU2/70Es7DVV5u0t6ndsmr88ibYwwPupGR/fhPCpDyssg\n7lFAEpOwnbKG9a7E7axHpXBRSIE54sh+ESyf6MHH/oyKOLhZ0v4PjRDKaKuMDRst\nMOClgNjD/4bzKpfWljuPYemXz1oIBQitBW5aXnCdsmdrmOLDQpz3qOgIo+RRiyki\nvVU5N54L65sj4WisLt1TT45wbhrkQUz+8GmhV5rHwQKBgQDow32/gSb/M0BKFk5y\n+pSeoLkYp/dwfBFWYT6CNBaKePARFVCdr3db8yOEQD4hrmTOU0EP9c4FSLcaa0Xy\n7n2crhhfZWFpIpRMyXhpeKpqdjQFimfBOK6cIdjnWrtJ6Ik3m4E9p7kKThIsInTc\n/TETwAyzFN+J2ADRdrUdCukOwQKBgQC/4+1Rk8a++Jr9Sznx+JH4vj/J2cGsu7uQ\n0nikcOAFO5HzG7+mt9Wv9/MiPtEYwmc7YziDXldKLpshT2m6wrS1uzzOXAnvXFAh\npiCXQsmVA3gmrVd53k+eZfqrZ1n/rL1kCewRS5LX8xIhM28VIkGqkVy4ZEifMotG\nZKSbH0b4jQKBgQCsJ6rh8Uw+hFGQel8be2pgyM8eBV1lvN213ca11oC1ei1U9Ubi\n2dyWDYa/UiSiFLJKSBlfDJaMIfQLfjwGKY6OS9WK+RjLAeBdysVcfPrOMw7W6j9D\nEgFTSVV8CAdt6qdSkZlNWLfrf0LBkdqNeFbMHMdHzLBo63HverUJ/f/SAQKBgHIk\n2t5T0T14FHnnbaiJ/ArC4J7pcVOWuJQFHs5ydk+mh8LdFrvNTsdF7tLIGwlnWpDx\nDITYcYQnBRBjdLkraONRZXI7PY2sk93wPCK+D7scPTSEmCxeGW5XqyyaZea4klAX\nttzy336lkHs/ZSxlHDqiDU2CGdDY+A//fgroKAdhAoGAA5FXfMzTQLGqxg4J2B1z\nFEXNbrqZZFGgKiveUhhZLm4zPiHXtZXvDtSLwGgcO8oGfTfYueTcHb/Eiar7mKv+\n+SqpAqkINJTthIFVIiD39S9jPFUXzBkf5ZJKPLKQArhzEGxen+SD6ZUO058fA94L\n9FblRGlMtr2o5z0NC7H5zaU=\n-----END PRIVATE KEY-----\n", + "client_email": "google-photos-api@brown-dash.iam.gserviceaccount.com", + "client_id": "112995422877175743408", + "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_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-photos-api%40brown-dash.iam.gserviceaccount.com" +} \ No newline at end of file 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..cea452f08 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.Glt2B3lsrpWxZ9DMg1RcTksAFzfR8dVWhf7d7tAvbJ4UbcSVO0Q3aYNGtaMKPtmxR24rH88iQSiKCL8S328TQFEN6LtZgvizymednK5EW0jNCvG6ecdZQ-vwcypR","refresh_token":"1/6X3oGYz4A0p8UW2IgsZ-GqTgQUY43S6Ahsaf_GQhSs8","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567360444627} \ No newline at end of file diff --git a/src/server/credentials/google_photos_credentials.ts b/src/server/credentials/google_photos_credentials.ts new file mode 100644 index 000000000..11c1c766c --- /dev/null +++ b/src/server/credentials/google_photos_credentials.ts @@ -0,0 +1,35 @@ +const config: any = {}; + +// The OAuth client ID from the Google Developers console. +config.oAuthClientID = '1005546247619-l40012sl4idpq17b5emielcs1delffog.apps.googleusercontent.com'; + +// The OAuth client secret from the Google Developers console. +config.oAuthclientSecret = 'xEUJ0OBvhlCKA6SLt8TvWBs3'; + +// The callback to use for OAuth requests. This is the URL where the app is +// running. For testing and running it locally, use 127.0.0.1. +config.oAuthCallbackUrl = 'http://localhost:1050/auth/google/callback'; + +// The port where the app should listen for requests. +config.port = 1050; + +// The scopes to request. The app requires the photoslibrary.readonly and +// plus.me scopes. +config.scopes = [ + 'https://www.googleapis.com/auth/photoslibrary.readonly', + 'profile', +]; + +// The number of photos to load for search requests. +config.photosToLoad = 150; + +// The page size to use for search requests. 100 is reccommended. +config.searchPageSize = 100; + +// The page size to use for the listing albums request. 50 is reccommended. +config.albumPageSize = 50; + +// The API end point to use. Do not change. +config.apiEndpoint = 'https://photoslibrary.googleapis.com'; + +module.exports = config; \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 34a0a19f1..6105dedcc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -29,6 +29,7 @@ import { RouteStore } from './RouteStore'; import v4 = require('uuid/v4'); const app = express(); const config = require('../../webpack.config'); +const OAuthConfig = require('../server/credentials/google_photos_credentials'); import { createCanvas, loadImage, Canvas } from "canvas"; const compiler = webpack(config); const port = 1050; // default port to listen @@ -46,6 +47,7 @@ import { GaxiosResponse } from 'gaxios'; import { Opt } from '../new_fields/Doc'; import { docs_v1 } from 'googleapis'; import { Endpoint } from 'googleapis-common'; +import { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -196,6 +198,31 @@ const solrURL = "http://localhost:8983/solr/#/dash"; // GETTERS +// app.get('/auth/google', passport.authenticate('google', { +// scope: OAuthConfig.scopes, +// failureFlash: true, // Display errors to the user. +// session: true, +// })); + +// app.get("/failed", (req, res) => res.send("DIDN'T WORK!")); + +// app.get( +// '/auth/google/callback', +// passport.authenticate( +// 'google', { failureRedirect: '/failed', failureFlash: true, session: true }), +// (req, res) => { +// // User has logged in. +// console.log('OAUTH: user has logged in 1.'); +// PhotosLibraryQuery(req.user.token, {}); +// console.log('OAUTH: user has logged in 2.'); +// res.redirect('/'); +// }); + +// app.get('/GooglePhotos', (req, res) => { +// console.log("WORKING ON GOOGLE PHOTOS"); +// PhotosLibraryQuery(req.user.token, {}); +// }); + app.get("/search", async (req, res) => { const solrQuery: any = {}; ["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]); 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 @@ /// -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 . - */ - class Document extends React.Component { } - - 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 can contain as many pages as - * you want, but ensure not rendering a page inside any component besides - * Document. - */ - class Page extends React.Component { } - - 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 { } - - 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 { } - - 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 { } - - 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 { } - - interface NoteProps extends NodeProps { - children: string; - } - - class Note extends React.Component { } - - interface BlobProviderParams { - blob: Blob | null; - url: string | null; - loading: boolean; - error: Error | null; - } - interface BlobProviderProps { - document: React.ReactElement; - 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 { } - - interface PDFViewerProps { - width?: number; - height?: number; - style?: Style | Style[]; - className?: string; - children?: React.ReactElement; - } - - /** - * Iframe PDF viewer for client-side generated documents. - * @platform web - */ - class PDFViewer extends React.Component { } - - interface PDFDownloadLinkProps { - document: React.ReactElement; - 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 . + */ + class Document extends React.Component { } + + 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 can contain as many pages as + * you want, but ensure not rendering a page inside any component besides + * Document. + */ + class Page extends React.Component { } + + 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 { } + + 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 { } + + 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 { } + + 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 { } + + interface NoteProps extends NodeProps { + children: string; + } + + class Note extends React.Component { } + + interface BlobProviderParams { + blob: Blob | null; + url: string | null; + loading: boolean; + error: Error | null; + } + interface BlobProviderProps { + document: React.ReactElement; + 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 { } + + interface PDFViewerProps { + width?: number; + height?: number; + style?: Style | Style[]; + className?: string; + children?: React.ReactElement; + } + + /** + * Iframe PDF viewer for client-side generated documents. + * @platform web + */ + class PDFViewer extends React.Component { } + + interface PDFDownloadLinkProps { + document: React.ReactElement; + 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 { } + + 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, + ) => Promise; + clear: () => void; + reset: () => void; + }; + + const StyleSheet: { + hairlineWidth: number; + create: (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, + ) => { + isDirty: () => boolean; + updateContainer: (document: React.ReactElement) => void; + toBuffer: () => NodeJS.ReadableStream; + toBlob: () => Blob; + toString: () => string; + }; + + const renderToStream: ( + document: React.ReactElement, + ) => NodeJS.ReadableStream; + + const renderToFile: ( + document: React.ReactElement, + filePath: string, + callback?: (output: NodeJS.ReadableStream, filePath: string) => any, + ) => Promise; + + 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 { } - - 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, - ) => Promise; - 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: (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, - ) => { - isDirty: () => boolean; - updateContainer: (document: React.ReactElement) => void; - toBuffer: () => NodeJS.ReadableStream; - toBlob: () => Blob; - toString: () => string; - }; - - const renderToStream: ( - document: React.ReactElement, - ) => NodeJS.ReadableStream; - - const renderToFile: ( - document: React.ReactElement, - filePath: string, - callback?: (output: NodeJS.ReadableStream, filePath: string) => any, - ) => Promise; - - 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 -- cgit v1.2.3-70-g09d2 From 5e12b7d816f1778af112ce69f3029e2f4a72bb08 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 1 Sep 2019 14:03:32 -0400 Subject: authentication working --- src/server/apis/google/GoogleApiServerUtils.ts | 25 ++++++++-------- src/server/apis/google/GooglePhotosUtils.ts | 22 +++++++------- src/server/authentication/config/passport.ts | 13 -------- src/server/credentials/auth.json | 12 -------- .../credentials/google_photos_credentials.ts | 35 ---------------------- src/server/index.ts | 11 ++++--- 6 files changed, 29 insertions(+), 89 deletions(-) delete mode 100644 src/server/credentials/auth.json delete mode 100644 src/server/credentials/google_photos_credentials.ts (limited to 'src/server') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 2fb44d9a2..c1bd3300e 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -1,7 +1,7 @@ import { google, docs_v1, slides_v1 } 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"; @@ -48,14 +48,14 @@ export namespace GoogleApiServerUtils { export const GetEndpoint = async (sector: string, paths: CredentialPaths) => { return new Promise>((resolve, reject) => { - readFile(paths.credentials, (err, credentials) => { + 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(async auth => { + authorize(parseBuffer(credentials), paths.token).then(async result => { let routed: Opt; - let parameters: EndpointParameters = { auth, version: "v1" }; + let parameters: EndpointParameters = { auth: result.client, version: "v1" }; switch (sector) { case Service.Documents: routed = google.docs(parameters).documents; @@ -64,7 +64,7 @@ export namespace GoogleApiServerUtils { routed = google.slides(parameters).presentations; break; case Service.Photos: - const photos = new Photos(auth); + const photos = new Photos(result.token.access_token); let response = await photos.albums.list(); console.log("WE GOT SOMETHING!", response); } @@ -74,24 +74,25 @@ export namespace GoogleApiServerUtils { }); }; - + 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 { + 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) => { + 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); + let parsed = parseBuffer(token); + oAuth2Client.setCredentials(parsed); + resolve({ token: parsed, client: oAuth2Client }); }); }); } @@ -103,7 +104,7 @@ export namespace GoogleApiServerUtils { * @param {getEventsCallback} callback The callback for the authorized client. */ function getNewToken(oAuth2Client: OAuth2Client, token_path: string) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const authUrl = oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: SCOPES.map(relative => prefix + relative), @@ -129,7 +130,7 @@ export namespace GoogleApiServerUtils { } console.log('Token stored to', token_path); }); - resolve(oAuth2Client); + resolve({ token, client: oAuth2Client }); }); }); }); diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts index c33ad2dd9..7f9ffb6f3 100644 --- a/src/server/apis/google/GooglePhotosUtils.ts +++ b/src/server/apis/google/GooglePhotosUtils.ts @@ -1,12 +1,12 @@ -import request = require('request-promise'); -const key = require("../../credentials/auth.json"); +// import request = require('request-promise'); +// const key = require("../../credentials/auth.json"); -export const PhotosLibraryQuery = async (authToken: any, parameters: any) => { - let options = { - headers: { 'Content-Type': 'application/json' }, - json: parameters, - auth: { 'bearer': authToken }, - }; - const result = await request.post(config.apiEndpoint + '/v1/mediaItems:search', options); - return result; -}; \ No newline at end of file +// export const PhotosLibraryQuery = async (authToken: any, parameters: any) => { +// let options = { +// headers: { 'Content-Type': 'application/json' }, +// json: parameters, +// auth: { 'bearer': authToken }, +// }; +// const result = await request.post(config.apiEndpoint + '/v1/mediaItems:search', options); +// return result; +// }; \ No newline at end of file diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 97ded8785..6e0e01b9e 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -5,7 +5,6 @@ import { default as User } from '../models/user_model'; import { Request, Response, NextFunction } from "express"; import { RouteStore } from '../../RouteStore'; import * as GoogleOAuth from "passport-google-oauth20"; -const config = require("../../credentials/google_photos_credentials"); const LocalStrategy = passportLocal.Strategy; const GoogleOAuthStrategy = GoogleOAuth.Strategy; @@ -34,18 +33,6 @@ passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true }); })); - -passport.use(new GoogleOAuthStrategy( - { - clientID: config.oAuthClientID, - clientSecret: config.oAuthclientSecret, - callbackURL: config.oAuthCallbackUrl, - // Set the correct profile URL that does not require any additional APIs - userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' - }, - (token, refreshToken, profile, done) => done(undefined, { profile, token }) -)); - export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => { if (req.isAuthenticated()) { return next(); diff --git a/src/server/credentials/auth.json b/src/server/credentials/auth.json deleted file mode 100644 index 557eca4b6..000000000 --- a/src/server/credentials/auth.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "service_account", - "project_id": "brown-dash", - "private_key_id": "ddf0473a9ac56956b5818e04a7ee406a64d5b0a6", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCueRfxic2oL9nr\nnWSLgl7XR/BKikm4p2sib6szaoTO+q6itcJgt2TDleK/7Y4KW/KhvCfhWVet0Hz0\nIDyg4N/gc2yxuDA6/m8DPWU9kDj8VFR7LVFawOKgo1WbgLcC0Qu8qHzAffrlg8si\nhj3vGuoS/YDn/mz0krwFmCfIx+S0lJ9a7FUjJL5C+CIwAEEYiU7xnTW7pVVNXAm/\n/YKD17ToAjREOtlfVVYO7tZ7V5BiW0I0jJvxw+t1pgrZZe7WPBSBJg9KKGIl+mRi\ndtUMR9Hyt3nMKNZIrSm0OkAz82HxfcapRdSB3wkVjoyW63YaTVHKoBOqRElfMtoM\nqu8wbhhNAgMBAAECggEAA41wJ8kg8J8peQMZ/b7gZvzuPy+h0M/J3j2MrG3rY9qA\nrUv1oqoBSXvhuNDhtEN12oYIqtg6m+L+Sas8CMuOC2rWPafM2u/80IGoGtDhtCjp\nv8inBX8ew4YSiL7IxdbTU2/70Es7DVV5u0t6ndsmr88ibYwwPupGR/fhPCpDyssg\n7lFAEpOwnbKG9a7E7axHpXBRSIE54sh+ESyf6MHH/oyKOLhZ0v4PjRDKaKuMDRst\nMOClgNjD/4bzKpfWljuPYemXz1oIBQitBW5aXnCdsmdrmOLDQpz3qOgIo+RRiyki\nvVU5N54L65sj4WisLt1TT45wbhrkQUz+8GmhV5rHwQKBgQDow32/gSb/M0BKFk5y\n+pSeoLkYp/dwfBFWYT6CNBaKePARFVCdr3db8yOEQD4hrmTOU0EP9c4FSLcaa0Xy\n7n2crhhfZWFpIpRMyXhpeKpqdjQFimfBOK6cIdjnWrtJ6Ik3m4E9p7kKThIsInTc\n/TETwAyzFN+J2ADRdrUdCukOwQKBgQC/4+1Rk8a++Jr9Sznx+JH4vj/J2cGsu7uQ\n0nikcOAFO5HzG7+mt9Wv9/MiPtEYwmc7YziDXldKLpshT2m6wrS1uzzOXAnvXFAh\npiCXQsmVA3gmrVd53k+eZfqrZ1n/rL1kCewRS5LX8xIhM28VIkGqkVy4ZEifMotG\nZKSbH0b4jQKBgQCsJ6rh8Uw+hFGQel8be2pgyM8eBV1lvN213ca11oC1ei1U9Ubi\n2dyWDYa/UiSiFLJKSBlfDJaMIfQLfjwGKY6OS9WK+RjLAeBdysVcfPrOMw7W6j9D\nEgFTSVV8CAdt6qdSkZlNWLfrf0LBkdqNeFbMHMdHzLBo63HverUJ/f/SAQKBgHIk\n2t5T0T14FHnnbaiJ/ArC4J7pcVOWuJQFHs5ydk+mh8LdFrvNTsdF7tLIGwlnWpDx\nDITYcYQnBRBjdLkraONRZXI7PY2sk93wPCK+D7scPTSEmCxeGW5XqyyaZea4klAX\nttzy336lkHs/ZSxlHDqiDU2CGdDY+A//fgroKAdhAoGAA5FXfMzTQLGqxg4J2B1z\nFEXNbrqZZFGgKiveUhhZLm4zPiHXtZXvDtSLwGgcO8oGfTfYueTcHb/Eiar7mKv+\n+SqpAqkINJTthIFVIiD39S9jPFUXzBkf5ZJKPLKQArhzEGxen+SD6ZUO058fA94L\n9FblRGlMtr2o5z0NC7H5zaU=\n-----END PRIVATE KEY-----\n", - "client_email": "google-photos-api@brown-dash.iam.gserviceaccount.com", - "client_id": "112995422877175743408", - "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_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-photos-api%40brown-dash.iam.gserviceaccount.com" -} \ No newline at end of file diff --git a/src/server/credentials/google_photos_credentials.ts b/src/server/credentials/google_photos_credentials.ts deleted file mode 100644 index 11c1c766c..000000000 --- a/src/server/credentials/google_photos_credentials.ts +++ /dev/null @@ -1,35 +0,0 @@ -const config: any = {}; - -// The OAuth client ID from the Google Developers console. -config.oAuthClientID = '1005546247619-l40012sl4idpq17b5emielcs1delffog.apps.googleusercontent.com'; - -// The OAuth client secret from the Google Developers console. -config.oAuthclientSecret = 'xEUJ0OBvhlCKA6SLt8TvWBs3'; - -// The callback to use for OAuth requests. This is the URL where the app is -// running. For testing and running it locally, use 127.0.0.1. -config.oAuthCallbackUrl = 'http://localhost:1050/auth/google/callback'; - -// The port where the app should listen for requests. -config.port = 1050; - -// The scopes to request. The app requires the photoslibrary.readonly and -// plus.me scopes. -config.scopes = [ - 'https://www.googleapis.com/auth/photoslibrary.readonly', - 'profile', -]; - -// The number of photos to load for search requests. -config.photosToLoad = 150; - -// The page size to use for search requests. 100 is reccommended. -config.searchPageSize = 100; - -// The page size to use for the listing albums request. 50 is reccommended. -config.albumPageSize = 50; - -// The API end point to use. Do not change. -config.apiEndpoint = 'https://photoslibrary.googleapis.com'; - -module.exports = config; \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 6105dedcc..1f105e9d2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -29,7 +29,6 @@ import { RouteStore } from './RouteStore'; import v4 = require('uuid/v4'); const app = express(); const config = require('../../webpack.config'); -const OAuthConfig = require('../server/credentials/google_photos_credentials'); import { createCanvas, loadImage, Canvas } from "canvas"; const compiler = webpack(config); const port = 1050; // default port to listen @@ -43,11 +42,11 @@ 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 { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils'; +// import { GaxiosResponse } from 'gaxios'; +// import { Opt } from '../new_fields/Doc'; +// import { docs_v1 } from 'googleapis'; +// import { Endpoint } from 'googleapis-common'; +// import { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); -- cgit v1.2.3-70-g09d2 From b8a6b72938804902a7e4478cde9c50339341f67d Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 1 Sep 2019 14:50:06 -0400 Subject: regenerated auth key --- src/server/apis/google/GoogleApiServerUtils.ts | 3 +-- src/server/credentials/google_docs_token.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src/server') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c1bd3300e..bc9ae2726 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -65,8 +65,7 @@ export namespace GoogleApiServerUtils { break; case Service.Photos: const photos = new Photos(result.token.access_token); - let response = await photos.albums.list(); - console.log("WE GOT SOMETHING!", response); + console.log(await photos.albums.list()); } resolve(routed); }); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index cea452f08..fcb5c8abc 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glt2B3lsrpWxZ9DMg1RcTksAFzfR8dVWhf7d7tAvbJ4UbcSVO0Q3aYNGtaMKPtmxR24rH88iQSiKCL8S328TQFEN6LtZgvizymednK5EW0jNCvG6ecdZQ-vwcypR","refresh_token":"1/6X3oGYz4A0p8UW2IgsZ-GqTgQUY43S6Ahsaf_GQhSs8","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567360444627} \ No newline at end of file +{"access_token":"ya29.Glt2ByOBCyO7DNKKXaiDbK5c5OMwRoprqiCksLCu93WKuAE-YQ0gDCZQCqP07WV6QH0gMpwn47ghico1h5Rkxh-ukJdY9ndRTv7rEKJY32To4__gZh4xKVhwfOvf","refresh_token":"1/ynmFZmA-yqA1sKU3wF-g6QxCx9wGSTIA2sOvIhC_Ps0","scope":"https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1567366717827} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 769b4c0b9ac61729b94b32999d3713a2dce53627 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 1 Sep 2019 17:02:11 -0400 Subject: added typings for google photo album manipulations --- src/server/apis/google/GoogleApiServerUtils.ts | 17 ++- src/server/apis/google/GooglePhotosUtils.ts | 177 +++++++++++++++++++++++-- src/server/authentication/config/passport.ts | 2 - src/server/credentials/google_docs_token.json | 2 +- 4 files changed, 180 insertions(+), 18 deletions(-) (limited to 'src/server') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index bc9ae2726..656984b8a 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -5,7 +5,8 @@ import { OAuth2Client, Credentials } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; -import Photos = require("googlephotos"); +import { GooglePhotos, CreateAlbum, Action } from "./GooglePhotosUtils"; +import { Utils } from "../../../Utils"; /** * Server side authentication for Google Api queries. @@ -64,8 +65,18 @@ export namespace GoogleApiServerUtils { routed = google.slides(parameters).presentations; break; case Service.Photos: - const photos = new Photos(result.token.access_token); - console.log(await photos.albums.list()); + let token = result.token.access_token; + if (token) { + let create: CreateAlbum = { + action: Action.Create, + body: { + album: { + title: "Sam's Bulk Export", + } + } + }; + console.log(await GooglePhotos.ExecuteQuery(token, create)); + } } resolve(routed); }); diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts index 7f9ffb6f3..439a41cb6 100644 --- a/src/server/apis/google/GooglePhotosUtils.ts +++ b/src/server/apis/google/GooglePhotosUtils.ts @@ -1,12 +1,165 @@ -// import request = require('request-promise'); -// const key = require("../../credentials/auth.json"); - -// export const PhotosLibraryQuery = async (authToken: any, parameters: any) => { -// let options = { -// headers: { 'Content-Type': 'application/json' }, -// json: parameters, -// auth: { 'bearer': authToken }, -// }; -// const result = await request.post(config.apiEndpoint + '/v1/mediaItems:search', options); -// return result; -// }; \ No newline at end of file +import request = require('request-promise'); + +const apiEndpoint = "https://photoslibrary.googleapis.com"; + +export type GooglePhotosQuery = AlbumsQuery; + +export type AlbumsQuery = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | CreateAlbum | GetAlbum | ListAlbum | ShareAlbum | UnshareAlbum) & { body: any }; + +export enum Action { + AddEnrichment, + BatchAddMediaItems, + BatchRemoveMediaItems, + Create, + Get, + List, + Share, + 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 CreateAlbum { + action: Action.Create; + body: { + album: AlbumTemplate; + }; +} + +export interface GetAlbum { + action: Action.Get; + albumId: string; +} + +export interface ListAlbum { + action: Action.List; + parameters: { + pageSize: number, + pageToken: string, + excludeNonAppCreatedData: boolean + }; +} + +export interface ShareAlbum { + action: Action.Share; + albumId: string; + body: { + sharedAlbumOptions: SharedAlbumOptions; + }; +} + +export interface UnshareAlbum { + action: Action.Unshare; + albumId: string; +} + +export interface AlbumTemplate { + title: string; +} + +export interface Album { + id: string; + title: string; + productUrl: string; + isWriteable: boolean; + shareInfo: ShareInfo; + mediaItemsCount: string; + coverPhotoBaseUrl: string; + coverPhotoMediaItemId: string; +} + +export interface ShareInfo { + sharedAlbumOptions: SharedAlbumOptions; + shareableUrl: string; + shareToken: string; + isJoined: boolean; + isOwned: boolean; +} + +export interface SharedAlbumOptions { + isCollaborative: boolean; + isCommentable: boolean; +} + +export enum PositionType { + POSITION_TYPE_UNSPECIFIED, + FIRST_IN_ALBUM, + LAST_IN_ALBUM, + AFTER_MEDIA_ITEM, + AFTER_ENRICHMENT_ITEM +} + +export type AlbumPosition = 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 } + }; +} + +export namespace GooglePhotos { + + export const ExecuteQuery = async (authToken: string, query: AlbumsQuery) => { + let options = { + headers: { 'Content-Type': 'application/json' }, + auth: { 'bearer': authToken }, + body: query.body, + json: true + }; + const result = await request.post(apiEndpoint + '/v1/albums', options); + return result; + }; + +} diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 6e0e01b9e..e5733cbb5 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -4,10 +4,8 @@ import _ from "lodash"; import { default as User } from '../models/user_model'; import { Request, Response, NextFunction } from "express"; import { RouteStore } from '../../RouteStore'; -import * as GoogleOAuth from "passport-google-oauth20"; const LocalStrategy = passportLocal.Strategy; -const GoogleOAuthStrategy = GoogleOAuth.Strategy; passport.serializeUser((user, done) => { done(undefined, user.id); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index fcb5c8abc..61864512c 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glt2ByOBCyO7DNKKXaiDbK5c5OMwRoprqiCksLCu93WKuAE-YQ0gDCZQCqP07WV6QH0gMpwn47ghico1h5Rkxh-ukJdY9ndRTv7rEKJY32To4__gZh4xKVhwfOvf","refresh_token":"1/ynmFZmA-yqA1sKU3wF-g6QxCx9wGSTIA2sOvIhC_Ps0","scope":"https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1567366717827} \ No newline at end of file +{"access_token":"ya29.Glt3B8HoVEda7Ab5TQMVrfvjPN2fFp4sFHtGoDs3TsBgFfw4G208q90JiFjkmQqwODjJi3sf4NCZd78VZTVL3aI0By7_ElZF7XaCvA0LJnfcAi2gi1P-2-boyjYO","refresh_token":"1/tJOVDbPZlADzd2B8Q2_j7jqignXlRwHsU7LbZkdbDBc","scope":"https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567374969108} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From d1960ed915e78014592567e16dd1de9545781a27 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 1 Sep 2019 17:12:45 -0400 Subject: factored out typings --- src/server/apis/google/GoogleApiServerUtils.ts | 7 +- src/server/apis/google/GooglePhotosUtils.ts | 152 +------------------------ src/server/apis/google/typings/albums.ts | 148 ++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 151 deletions(-) create mode 100644 src/server/apis/google/typings/albums.ts (limited to 'src/server') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 656984b8a..c6d901577 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -5,8 +5,9 @@ import { OAuth2Client, Credentials } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; -import { GooglePhotos, CreateAlbum, Action } from "./GooglePhotosUtils"; +import { GooglePhotos } from "./GooglePhotosUtils"; import { Utils } from "../../../Utils"; +import { Album } from "./Typings/albums"; /** * Server side authentication for Google Api queries. @@ -67,8 +68,8 @@ export namespace GoogleApiServerUtils { case Service.Photos: let token = result.token.access_token; if (token) { - let create: CreateAlbum = { - action: Action.Create, + let create: Album.Create = { + action: Album.Action.Create, body: { album: { title: "Sam's Bulk Export", diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts index 439a41cb6..d4f16fd5d 100644 --- a/src/server/apis/google/GooglePhotosUtils.ts +++ b/src/server/apis/google/GooglePhotosUtils.ts @@ -1,157 +1,13 @@ import request = require('request-promise'); +import { Album } from './Typings/albums'; const apiEndpoint = "https://photoslibrary.googleapis.com"; -export type GooglePhotosQuery = AlbumsQuery; - -export type AlbumsQuery = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | CreateAlbum | GetAlbum | ListAlbum | ShareAlbum | UnshareAlbum) & { body: any }; - -export enum Action { - AddEnrichment, - BatchAddMediaItems, - BatchRemoveMediaItems, - Create, - Get, - List, - Share, - 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 CreateAlbum { - action: Action.Create; - body: { - album: AlbumTemplate; - }; -} - -export interface GetAlbum { - action: Action.Get; - albumId: string; -} - -export interface ListAlbum { - action: Action.List; - parameters: { - pageSize: number, - pageToken: string, - excludeNonAppCreatedData: boolean - }; -} - -export interface ShareAlbum { - action: Action.Share; - albumId: string; - body: { - sharedAlbumOptions: SharedAlbumOptions; - }; -} - -export interface UnshareAlbum { - action: Action.Unshare; - albumId: string; -} - -export interface AlbumTemplate { - title: string; -} - -export interface Album { - id: string; - title: string; - productUrl: string; - isWriteable: boolean; - shareInfo: ShareInfo; - mediaItemsCount: string; - coverPhotoBaseUrl: string; - coverPhotoMediaItemId: string; -} - -export interface ShareInfo { - sharedAlbumOptions: SharedAlbumOptions; - shareableUrl: string; - shareToken: string; - isJoined: boolean; - isOwned: boolean; -} - -export interface SharedAlbumOptions { - isCollaborative: boolean; - isCommentable: boolean; -} - -export enum PositionType { - POSITION_TYPE_UNSPECIFIED, - FIRST_IN_ALBUM, - LAST_IN_ALBUM, - AFTER_MEDIA_ITEM, - AFTER_ENRICHMENT_ITEM -} - -export type AlbumPosition = 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 } - }; -} - export namespace GooglePhotos { - export const ExecuteQuery = async (authToken: string, query: AlbumsQuery) => { + export type Query = Album.Query; + + export const ExecuteQuery = async (authToken: string, query: GooglePhotos.Query) => { let options = { headers: { 'Content-Type': 'application/json' }, auth: { 'bearer': authToken }, diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts new file mode 100644 index 000000000..1c9b379fe --- /dev/null +++ b/src/server/apis/google/typings/albums.ts @@ -0,0 +1,148 @@ +export namespace Album { + + export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare) & { body: any }; + + export enum Action { + AddEnrichment, + BatchAddMediaItems, + BatchRemoveMediaItems, + Create, + Get, + List, + Share, + 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: { + 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 -- cgit v1.2.3-70-g09d2 From 8b992ef2c152e86299fd3460112124d476393a60 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 1 Sep 2019 17:13:09 -0400 Subject: tweaks --- src/server/apis/google/GoogleApiServerUtils.ts | 2 +- src/server/apis/google/GooglePhotosUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/server') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c6d901577..00e289b00 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -7,7 +7,7 @@ import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; import { GooglePhotos } from "./GooglePhotosUtils"; import { Utils } from "../../../Utils"; -import { Album } from "./Typings/albums"; +import { Album } from "./typings/albums"; /** * Server side authentication for Google Api queries. diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts index d4f16fd5d..750630626 100644 --- a/src/server/apis/google/GooglePhotosUtils.ts +++ b/src/server/apis/google/GooglePhotosUtils.ts @@ -1,5 +1,5 @@ import request = require('request-promise'); -import { Album } from './Typings/albums'; +import { Album } from './typings/albums'; const apiEndpoint = "https://photoslibrary.googleapis.com"; -- cgit v1.2.3-70-g09d2 From 38176e5ba949b84dc410d29197180121d81e085c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 3 Sep 2019 12:51:59 -0400 Subject: implemented refresh tokens and create, get, list --- .../apis/google_docs/GooglePhotosClientUtils.ts | 35 +++++++++ src/client/views/MainView.tsx | 10 ++- src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 90 ++++++++++++++-------- src/server/apis/google/GooglePhotosServerUtils.ts | 68 ++++++++++++++++ src/server/apis/google/GooglePhotosUploadUtils.ts | 42 ++++++++++ src/server/apis/google/GooglePhotosUtils.ts | 21 ----- src/server/apis/google/typings/albums.ts | 30 ++++---- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 54 ++++++------- 10 files changed, 252 insertions(+), 103 deletions(-) create mode 100644 src/client/apis/google_docs/GooglePhotosClientUtils.ts create mode 100644 src/server/apis/google/GooglePhotosServerUtils.ts create mode 100644 src/server/apis/google/GooglePhotosUploadUtils.ts delete mode 100644 src/server/apis/google/GooglePhotosUtils.ts (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts new file mode 100644 index 000000000..67a282f48 --- /dev/null +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -0,0 +1,35 @@ +import { Album } from "../../../server/apis/google/typings/albums"; +import { PostToServer } from "../../../Utils"; +import { RouteStore } from "../../../server/RouteStore"; + +export namespace GooglePhotosClientUtils { + + export const Create = async (title: string) => { + let parameters = { + action: Album.Action.Create, + body: { album: { title } } + } as Album.Create; + return PostToServer(RouteStore.googlePhotos, parameters); + }; + + export const List = async (options?: Partial) => { + let parameters = { + action: Album.Action.List, + parameters: { + pageSize: (options ? options.pageSize : 20) || 20, + pageToken: (options ? options.pageToken : undefined) || undefined, + excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false, + } as Album.ListOptions + } as Album.List; + return PostToServer(RouteStore.googlePhotos, parameters); + }; + + export const Get = async (albumId: string) => { + let parameters = { + action: Album.Action.Get, + albumId + } as Album.Get; + return PostToServer(RouteStore.googlePhotos, parameters); + }; + +} \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index df0718072..ece475c80 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -42,6 +42,8 @@ import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils'; import { docs_v1 } from 'googleapis'; +import { Album } from '../../server/apis/google/typings/albums'; +import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils'; @observer export class MainView extends React.Component { @@ -128,7 +130,7 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - PostToServer('/googleDocs/Photos/Test', {}); + this.executeGooglePhotosAlbumTestRoutine(); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -147,6 +149,12 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } + executeGooglePhotosAlbumTestRoutine = async () => { + let title = "This is a generically created album!"; + console.log(await GooglePhotosClientUtils.Create(title)); + console.log(await GooglePhotosClientUtils.List({ pageSize: 50 })); + } + componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); //close presentation diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 014906054..fc5511f98 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -31,6 +31,7 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", - googleDocs = "/googleDocs" + googleDocs = "/googleDocs", + googlePhotos = "/googlePhotos" } \ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 00e289b00..048ac4b21 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -1,13 +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, Credentials } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; -import { GooglePhotos } from "./GooglePhotosUtils"; -import { Utils } from "../../../Utils"; -import { Album } from "./typings/albums"; +import request = require('request-promise'); +import * as qs from 'query-string'; /** * Server side authentication for Google Api queries. @@ -31,8 +30,7 @@ export namespace GoogleApiServerUtils { export enum Service { Documents = "Documents", - Slides = "Slides", - Photos = "Photos" + Slides = "Slides" } export interface CredentialPaths { @@ -49,38 +47,31 @@ export namespace GoogleApiServerUtils { export type EndpointParameters = GlobalOptions & { version: "v1" }; export const GetEndpoint = async (sector: string, paths: CredentialPaths) => { - return new Promise>((resolve, reject) => { + return new Promise>(resolve => { + RetrieveAuthenticationInformation(paths).then(authentication => { + let routed: Opt; + 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 RetrieveAuthenticationInformation = async (paths: CredentialPaths) => { + return new Promise((resolve, reject) => { readFile(paths.credentials, async (err, credentials) => { if (err) { reject(err); return console.log('Error loading client secret file:', err); } - authorize(parseBuffer(credentials), paths.token).then(async result => { - let routed: Opt; - let parameters: EndpointParameters = { auth: result.client, version: "v1" }; - switch (sector) { - case Service.Documents: - routed = google.docs(parameters).documents; - break; - case Service.Slides: - routed = google.slides(parameters).presentations; - break; - case Service.Photos: - let token = result.token.access_token; - if (token) { - let create: Album.Create = { - action: Album.Action.Create, - body: { - album: { - title: "Sam's Bulk Export", - } - } - }; - console.log(await GooglePhotos.ExecuteQuery(token, create)); - } - } - resolve(routed); - }); + authorize(parseBuffer(credentials), paths.token).then(resolve, reject); }); }); }; @@ -101,13 +92,44 @@ export namespace GoogleApiServerUtils { if (err) { return getNewToken(oAuth2Client, token_path).then(resolve, reject); } - let parsed = parseBuffer(token); + 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((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; + 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. 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 => { + let action = parameters.query.action; + let dispatch = SuffixMap.get(action)!; + let suffix = Suffix(parameters, dispatch, action); + if (suffix) { + let query: any = parameters.query; + let options: any = { + headers: { 'Content-Type': 'application/json' }, + auth: { 'bearer': parameters.token }, + }; + if (query.body) { + options.body = query.body; + options.json = true; + } + let queryParameters = query.parameters; + if (queryParameters) { + suffix += `?${qs.stringify(queryParameters)}`; + } + let dispatcher = dispatch.method === "POST" ? request.post : request.get; + return dispatcher(apiEndpoint + suffix, options); + } + }; + + const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => { + let query: any = parameters.query; + let id = query.albumId; + let suffix = 'albums'; + if (dispatch.required) { + if (!id) { + return undefined; + } + suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`; + } + return suffix; + }; + + const SuffixMap = new Map([ + [Album.Action.AddEnrichment, { required: true, method: "POST" }], + [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }], + [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }], + [Album.Action.Create, { required: false, method: "POST" }], + [Album.Action.Get, { required: true, ignore: true, method: "GET" }], + [Album.Action.List, { required: false, method: "GET" }], + [Album.Action.Share, { required: true, method: "POST" }], + [Album.Action.Unshare, { required: true, method: "POST" }] + ]); + +} diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts new file mode 100644 index 000000000..2e1599aaf --- /dev/null +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -0,0 +1,42 @@ +import request = require('request-promise'); +import { Authorization } from './GooglePhotosServerUtils'; + +export namespace GooglePhotosUploadUtils { + + interface UploadInformation { + title: string; + url: URL; + } + + const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads"; + + export const SubmitUpload = async (parameters: Authorization & UploadInformation) => { + let MEDIA_BINARY_DATA = binary(parameters.url.href); + + let options = { + headers: { + 'Content-Type': 'application/octet-stream', + Authorization: { 'bearer': parameters.token }, + 'X-Goog-Upload-File-Name': parameters.title, + 'X-Goog-Upload-Protocol': 'raw' + }, + body: { MEDIA_BINARY_DATA }, + json: true + }; + const result = await request.post(apiEndpoint, options); + return result; + }; + + const binary = (source: string) => { + const image = document.createElement("img"); + image.src = source; + 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/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts deleted file mode 100644 index 750630626..000000000 --- a/src/server/apis/google/GooglePhotosUtils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import request = require('request-promise'); -import { Album } from './typings/albums'; - -const apiEndpoint = "https://photoslibrary.googleapis.com"; - -export namespace GooglePhotos { - - export type Query = Album.Query; - - export const ExecuteQuery = async (authToken: string, query: GooglePhotos.Query) => { - let options = { - headers: { 'Content-Type': 'application/json' }, - auth: { 'bearer': authToken }, - body: query.body, - json: true - }; - const result = await request.post(apiEndpoint + '/v1/albums', options); - return result; - }; - -} diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts index 1c9b379fe..f3025567d 100644 --- a/src/server/apis/google/typings/albums.ts +++ b/src/server/apis/google/typings/albums.ts @@ -1,16 +1,16 @@ export namespace Album { - export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare) & { body: any }; + export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare); export enum Action { - AddEnrichment, - BatchAddMediaItems, - BatchRemoveMediaItems, - Create, - Get, - List, - Share, - Unshare + AddEnrichment = "addEnrichment", + BatchAddMediaItems = "batchAddMediaItems", + BatchRemoveMediaItems = "batchRemoveMediaItems", + Create = "create", + Get = "get", + List = "list", + Share = "share", + Unshare = "unshare" } export interface AddEnrichment { @@ -52,11 +52,13 @@ export namespace Album { export interface List { action: Action.List; - parameters: { - pageSize: number, - pageToken: string, - excludeNonAppCreatedData: boolean - }; + parameters: ListOptions; + } + + export interface ListOptions { + pageSize: number; + pageToken: string; + excludeNonAppCreatedData: boolean; } export interface Share { diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 61864512c..39e067c86 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glt3B8HoVEda7Ab5TQMVrfvjPN2fFp4sFHtGoDs3TsBgFfw4G208q90JiFjkmQqwODjJi3sf4NCZd78VZTVL3aI0By7_ElZF7XaCvA0LJnfcAi2gi1P-2-boyjYO","refresh_token":"1/tJOVDbPZlADzd2B8Q2_j7jqignXlRwHsU7LbZkdbDBc","scope":"https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567374969108} \ No newline at end of file +{"access_token":"ya29.Glx4B9p8tvFsmsD-AD-D4jygL_YZVCFpPewM9djtfOT3T4S6ROxN5r0WLAKTYVNnXQbUri3Gu_-vIb0NWq9wEy1TdFTLIM8azWD82X5-I5BQq2DSOsYiKugvgVoHLw","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":1567529401142} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 1f105e9d2..54b954cfb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,11 +42,7 @@ 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 { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils'; +import { GooglePhotos } from './apis/google/GooglePhotosServerUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -195,32 +191,7 @@ app.get("/version", (req, res) => { // SEARCH const solrURL = "http://localhost:8983/solr/#/dash"; -// GETTERS - -// app.get('/auth/google', passport.authenticate('google', { -// scope: OAuthConfig.scopes, -// failureFlash: true, // Display errors to the user. -// session: true, -// })); - -// app.get("/failed", (req, res) => res.send("DIDN'T WORK!")); - -// app.get( -// '/auth/google/callback', -// passport.authenticate( -// 'google', { failureRedirect: '/failed', failureFlash: true, session: true }), -// (req, res) => { -// // User has logged in. -// console.log('OAUTH: user has logged in 1.'); -// PhotosLibraryQuery(req.user.token, {}); -// console.log('OAUTH: user has logged in 2.'); -// res.redirect('/'); -// }); - -// app.get('/GooglePhotos', (req, res) => { -// console.log("WORKING ON GOOGLE PHOTOS"); -// PhotosLibraryQuery(req.user.token, {}); -// }); +// GETTERSå app.get("/search", async (req, res) => { const solrQuery: any = {}; @@ -853,6 +824,27 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); +app.post(RouteStore.googlePhotos, (req, res) => { + GoogleApiServerUtils.RetrieveAuthenticationInformation({ credentials, token }).then(authentication => { + let validated = authentication.token.access_token; + if (!validated) { + res.send("Error: unable to authenticate Google Photos API request."); + return; + } + GooglePhotos.ExecuteQuery({ token: validated, 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}`); + }); + }); +}); + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", -- cgit v1.2.3-70-g09d2 From 0724d62e2a6797c640e79b46b678bd1d3917147f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 3 Sep 2019 13:18:23 -0400 Subject: fixed refresh token issue --- src/client/views/MainView.tsx | 8 -------- src/server/apis/google/GoogleApiServerUtils.ts | 2 +- src/server/credentials/google_docs_token.json | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) (limited to 'src/server') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ece475c80..3ee62ae86 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -130,8 +130,6 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - this.executeGooglePhotosAlbumTestRoutine(); - reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; let recent = CurrentUserUtils.UserDocument.recentlyClosed; @@ -149,12 +147,6 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } - executeGooglePhotosAlbumTestRoutine = async () => { - let title = "This is a generically created album!"; - console.log(await GooglePhotosClientUtils.Create(title)); - console.log(await GooglePhotosClientUtils.List({ pageSize: 50 })); - } - componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); //close presentation diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 048ac4b21..edfd89de4 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -116,7 +116,7 @@ export namespace GoogleApiServerUtils { request.post(url, headerParameters).then(response => { let parsed = JSON.parse(response); credentials.access_token = parsed.access_token; - credentials.expiry_date = new Date().getTime() + parsed.expires_in; + credentials.expiry_date = new Date().getTime() + parsed.expires_in * 1000; writeFile(token_path, JSON.stringify(credentials), (err) => { if (err) { console.error(err); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 39e067c86..c83d82607 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx4B9p8tvFsmsD-AD-D4jygL_YZVCFpPewM9djtfOT3T4S6ROxN5r0WLAKTYVNnXQbUri3Gu_-vIb0NWq9wEy1TdFTLIM8azWD82X5-I5BQq2DSOsYiKugvgVoHLw","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":1567529401142} \ No newline at end of file +{"access_token":"ya29.Glx4B4EQ5Q7WNw1hPrcqbZHssp0l1BtzszwNgmKGd783VBP9G3hLMe1AkbQw_Dl0amzgFeO29R4WP0ca-5U_Rf1tjsdtio6XjBGpJrY-S5wK7t71raKkwRPZFITw2Q","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":1567531020435} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 7e87aa4b7e0125482c87ab61f4a0de14e774558d Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 3 Sep 2019 16:23:42 -0400 Subject: working on media uploading --- .../apis/google_docs/GooglePhotosClientUtils.ts | 22 ++++++++-- src/client/views/MainView.tsx | 6 +++ src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 13 +++++- src/server/apis/google/GooglePhotosUploadUtils.ts | 20 ++------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 50 ++++++++++++++-------- 7 files changed, 73 insertions(+), 43 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 67a282f48..0864ebdb1 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,6 +1,7 @@ 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 { @@ -9,7 +10,7 @@ export namespace GooglePhotosClientUtils { action: Album.Action.Create, body: { album: { title } } } as Album.Create; - return PostToServer(RouteStore.googlePhotos, parameters); + return PostToServer(RouteStore.googlePhotosQuery, parameters); }; export const List = async (options?: Partial) => { @@ -21,7 +22,7 @@ export namespace GooglePhotosClientUtils { excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false, } as Album.ListOptions } as Album.List; - return PostToServer(RouteStore.googlePhotos, parameters); + return PostToServer(RouteStore.googlePhotosQuery, parameters); }; export const Get = async (albumId: string) => { @@ -29,7 +30,22 @@ export namespace GooglePhotosClientUtils { action: Album.Action.Get, albumId } as Album.Get; - return PostToServer(RouteStore.googlePhotos, parameters); + 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/views/MainView.tsx b/src/client/views/MainView.tsx index 3ee62ae86..590addaa3 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -44,6 +44,7 @@ import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils'; 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'; @observer export class MainView extends React.Component { @@ -130,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/server/RouteStore.ts b/src/server/RouteStore.ts index fc5511f98..3b3fd9b4a 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,6 +32,7 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotos = "/googlePhotos" + 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 edfd89de4..2c9085ebb 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -48,7 +48,7 @@ export namespace GoogleApiServerUtils { export const GetEndpoint = async (sector: string, paths: CredentialPaths) => { return new Promise>(resolve => { - RetrieveAuthenticationInformation(paths).then(authentication => { + RetrieveCredentials(paths).then(authentication => { let routed: Opt; let parameters: EndpointParameters = { auth: authentication.client, version: "v1" }; switch (sector) { @@ -64,7 +64,7 @@ export namespace GoogleApiServerUtils { }); }; - export const RetrieveAuthenticationInformation = async (paths: CredentialPaths) => { + export const RetrieveCredentials = async (paths: CredentialPaths) => { return new Promise((resolve, reject) => { readFile(paths.credentials, async (err, credentials) => { if (err) { @@ -76,6 +76,15 @@ export namespace GoogleApiServerUtils { }); }; + export const RetrieveAccessToken = async (paths: CredentialPaths) => { + return new Promise((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 diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 2e1599aaf..b358f9698 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -5,38 +5,24 @@ export namespace GooglePhotosUploadUtils { interface UploadInformation { title: string; - url: URL; + MEDIA_BINARY_DATA: string; } const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads"; export const SubmitUpload = async (parameters: Authorization & UploadInformation) => { - let MEDIA_BINARY_DATA = binary(parameters.url.href); - let options = { headers: { 'Content-Type': 'application/octet-stream', - Authorization: { 'bearer': parameters.token }, + auth: { 'bearer': parameters.token }, 'X-Goog-Upload-File-Name': parameters.title, 'X-Goog-Upload-Protocol': 'raw' }, - body: { MEDIA_BINARY_DATA }, + body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA }, json: true }; const result = await request.post(apiEndpoint, options); return result; }; - const binary = (source: string) => { - const image = document.createElement("img"); - image.src = source; - 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/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index c83d82607..e4be9ff60 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx4B4EQ5Q7WNw1hPrcqbZHssp0l1BtzszwNgmKGd783VBP9G3hLMe1AkbQw_Dl0amzgFeO29R4WP0ca-5U_Rf1tjsdtio6XjBGpJrY-S5wK7t71raKkwRPZFITw2Q","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":1567531020435} \ 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 54b954cfb..3e85b1ce0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -43,6 +43,7 @@ import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; import { GooglePhotos } from './apis/google/GooglePhotosServerUtils'; +import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -824,25 +825,36 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.post(RouteStore.googlePhotos, (req, res) => { - GoogleApiServerUtils.RetrieveAuthenticationInformation({ credentials, token }).then(authentication => { - let validated = authentication.token.access_token; - if (!validated) { - res.send("Error: unable to authenticate Google Photos API request."); - return; - } - GooglePhotos.ExecuteQuery({ token: validated, 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}`); - }); - }); +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)]) } = { -- cgit v1.2.3-70-g09d2 From 0c02b2ff3a41697c43d0aed98f330bd0293ef761 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 3 Sep 2019 19:21:23 -0400 Subject: fixed upload authentication issue, need to fix image byte extraction --- src/server/apis/google/GoogleApiServerUtils.ts | 1 + src/server/apis/google/GooglePhotosUploadUtils.ts | 2 +- src/server/credentials/google_docs_token.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src/server') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 2c9085ebb..b6330a609 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -23,6 +23,7 @@ export namespace GoogleApiServerUtils { 'drive', 'drive.file', 'photoslibrary', + 'photoslibrary.appendonly', 'photoslibrary.sharing' ]; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index b358f9698..cd2a586eb 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -14,7 +14,7 @@ export namespace GooglePhotosUploadUtils { let options = { headers: { 'Content-Type': 'application/octet-stream', - auth: { 'bearer': parameters.token }, + Authorization: `Bearer ${parameters.token}`, 'X-Goog-Upload-File-Name': parameters.title, 'X-Goog-Upload-Protocol': 'raw' }, diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index e4be9ff60..2ac972ed8 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"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 +{"access_token":"ya29.Glt5ByifP30HMz6a1fEG77qZlz9fBEOCz4PQ1VA8t_Ck2ZTPJKoyr6xc3-GFZISAqrrw2U8XpMyZv02_URfPwUX0Z_tMdlIFqsygowR-uClukbgQPNtgxp2LS1oW","refresh_token":"1/wK1cUVLnt581ba_pYGPdlTvAa-OS64nB5m5XOXEBJ8Q","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567556163894} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 5d59e9a379417140e10778cd43e8f87ecb816c37 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 6 Sep 2019 06:29:26 -0400 Subject: lightly tested functional export of any image doc (web url or stored in server public folder) to google photos, optionally stored in a given target album --- package.json | 2 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 73 +++++----- src/client/views/MainView.tsx | 17 ++- src/client/views/nodes/FormattedTextBox.tsx | 1 + src/server/RouteStore.ts | 2 +- src/server/apis/google/GoogleApiServerUtils.ts | 28 ++-- src/server/apis/google/GooglePhotosServerUtils.ts | 68 ---------- src/server/apis/google/GooglePhotosUploadUtils.ts | 122 +++++++++++++++-- src/server/apis/google/typings/albums.ts | 150 --------------------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 75 +++++------ 11 files changed, 206 insertions(+), 334 deletions(-) delete mode 100644 src/server/apis/google/GooglePhotosServerUtils.ts delete mode 100644 src/server/apis/google/typings/albums.ts (limited to 'src/server') diff --git a/package.json b/package.json index f0f2b467e..f56e34ce0 100644 --- a/package.json +++ b/package.json @@ -224,4 +224,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index b95cc98c9..2b72800a9 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,53 +1,42 @@ -import { Album } from "../../../server/apis/google/typings/albums"; -import { PostToServer } from "../../../Utils"; +import { PostToServer, Utils } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { ImageField } from "../../../new_fields/URLField"; +import { StrCast, Cast } from "../../../new_fields/Types"; +import { Doc, Opt } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import requestImageSize = require('../../util/request-image-size'); +import Photos = require('googlephotos'); export namespace GooglePhotosClientUtils { - export const Create = async (title: string) => { - let parameters = { - action: Album.Action.Create, - body: { album: { title } } - } as Album.Create; - return PostToServer(RouteStore.googlePhotosQuery, parameters); - }; + export type AlbumReference = { id: string } | { title: string }; + export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text())); - export const List = async (options?: Partial) => { - let parameters = { - action: Album.Action.List, - parameters: { - pageSize: (options ? options.pageSize : 20) || 20, - pageToken: (options ? options.pageToken : undefined) || undefined, - excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false, - } as Album.ListOptions - } as Album.List; - return PostToServer(RouteStore.googlePhotosQuery, parameters); - }; + export interface MediaInput { + description: string; + source: string; + } - export const Get = async (albumId: string) => { - let parameters = { - action: Album.Action.Get, - albumId - } as Album.Get; - return PostToServer(RouteStore.googlePhotosQuery, parameters); - }; - - export const toDataURL = (field: ImageField | undefined) => { - if (!field) { - return undefined; + export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => { + if (album && "title" in album) { + album = (await endpoint()).albums.create(album.title); + } + const media: MediaInput[] = []; + sources.forEach(document => { + const data = Cast(Doc.GetProto(document).data, ImageField); + const description = StrCast(document.caption); + if (!data) { + return undefined; + } + media.push({ + source: data.url.href, + description, + } as MediaInput); + }); + if (media.length) { + return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); } - const image = document.createElement("img"); - image.src = field.url.href; - image.width = 200; - image.height = 200; - const canvas = document.createElement("canvas"); - canvas.width = image.width; - canvas.height = image.height; - const ctx = canvas.getContext("2d")!; - ctx.drawImage(image, 0, 0); - const dataUrl = canvas.toDataURL("image/png"); - return dataUrl.replace(/^data:image\/(png|jpg);base64,/, ""); + return undefined; }; } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c1c95fc88..6d366216e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -40,11 +40,10 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; -import { docs_v1 } from 'googleapis'; -import { Album } from '../../server/apis/google/typings/albums'; import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../new_fields/URLField'; import { LinkFollowBox } from './linking/LinkFollowBox'; +import { DocumentManager } from '../util/DocumentManager'; @observer export class MainView extends React.Component { @@ -131,10 +130,7 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let image = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); - let parameters = { title: StrCast(image.title), MEDIA_BINARY_DATA: GooglePhotosClientUtils.toDataURL(Cast(image.data, ImageField)) }; - // PostToServer(RouteStore.googlePhotosMediaUpload, parameters).then(console.log); + this.executeGooglePhotosRoutine(); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -153,6 +149,15 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } + executeGooglePhotosRoutine = async () => { + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + doc.caption = "Well isn't this a nice cat image!"; + let photos = await GooglePhotosClientUtils.endpoint(); + let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; + console.log(await GooglePhotosClientUtils.UploadMedia([doc], { id: albumId })); + } + componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); //close presentation diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index b671d06ea..fda9ea33f 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -133,6 +133,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.isOverlay) { DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); } + FormattedTextBox.Instance = this; } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 3b3fd9b4a..b221b71bc 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,7 +32,7 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotosQuery = "/googlePhotosQuery", + googlePhotosAccessToken = "/googlePhotosAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload" } \ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index b6330a609..ac8023ce1 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -7,6 +7,7 @@ import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; import request = require('request-promise'); import * as qs from 'query-string'; +import Photos = require('googlephotos'); /** * Server side authentication for Google Api queries. @@ -35,19 +36,19 @@ export namespace GoogleApiServerUtils { } export interface CredentialPaths { - credentials: string; - token: string; + credentialsPath: string; + tokenPath: string; } export type ApiResponse = Promise; - export type ApiRouter = (endpoint: Endpoint, paramters: any) => ApiResponse; + export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse; export type ApiHandler = (parameters: any) => ApiResponse; export type Action = "create" | "retrieve" | "update"; export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler }; export type EndpointParameters = GlobalOptions & { version: "v1" }; - export const GetEndpoint = async (sector: string, paths: CredentialPaths) => { + export const GetEndpoint = (sector: string, paths: CredentialPaths) => { return new Promise>(resolve => { RetrieveCredentials(paths).then(authentication => { let routed: Opt; @@ -65,19 +66,19 @@ export namespace GoogleApiServerUtils { }); }; - export const RetrieveCredentials = async (paths: CredentialPaths) => { + export const RetrieveCredentials = (paths: CredentialPaths) => { return new Promise((resolve, reject) => { - readFile(paths.credentials, async (err, credentials) => { + readFile(paths.credentialsPath, async (err, credentials) => { if (err) { reject(err); return console.log('Error loading client secret file:', err); } - authorize(parseBuffer(credentials), paths.token).then(resolve, reject); + authorize(parseBuffer(credentials), paths.tokenPath).then(resolve, reject); }); }); }; - export const RetrieveAccessToken = async (paths: CredentialPaths) => { + export const RetrieveAccessToken = (paths: CredentialPaths) => { return new Promise((resolve, reject) => { RetrieveCredentials(paths).then( credentials => resolve(credentials.token.access_token!), @@ -86,6 +87,15 @@ export namespace GoogleApiServerUtils { }); }; + export const RetrievePhotosEndpoint = (paths: CredentialPaths) => { + return new Promise((resolve, reject) => { + RetrieveAccessToken(paths).then( + token => resolve(new Photos(token)), + reject + ); + }); + }; + type TokenResult = { token: Credentials, client: OAuth2Client }; /** * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client @@ -126,7 +136,7 @@ export namespace GoogleApiServerUtils { request.post(url, headerParameters).then(response => { let parsed = JSON.parse(response); credentials.access_token = parsed.access_token; - credentials.expiry_date = new Date().getTime() + parsed.expires_in * 1000; + credentials.expiry_date = new Date().getTime() + (parsed.expires_in * 1000); writeFile(token_path, JSON.stringify(credentials), (err) => { if (err) { console.error(err); diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts deleted file mode 100644 index cb5464abc..000000000 --- a/src/server/apis/google/GooglePhotosServerUtils.ts +++ /dev/null @@ -1,68 +0,0 @@ -import request = require('request-promise'); -import { Album } from './typings/albums'; -import * as qs from 'query-string'; - -const apiEndpoint = "https://photoslibrary.googleapis.com/v1/"; - -export interface Authorization { - token: string; -} - -export namespace GooglePhotos { - - export type Query = Album.Query; - export type QueryParameters = { query: GooglePhotos.Query }; - interface DispatchParameters { - required: boolean; - method: "GET" | "POST"; - ignore?: boolean; - } - - export const ExecuteQuery = async (parameters: Authorization & QueryParameters): Promise => { - let action = parameters.query.action; - let dispatch = SuffixMap.get(action)!; - let suffix = Suffix(parameters, dispatch, action); - if (suffix) { - let query: any = parameters.query; - let options: any = { - headers: { 'Content-Type': 'application/json' }, - auth: { 'bearer': parameters.token }, - }; - if (query.body) { - options.body = query.body; - options.json = true; - } - let queryParameters = query.parameters; - if (queryParameters) { - suffix += `?${qs.stringify(queryParameters)}`; - } - let dispatcher = dispatch.method === "POST" ? request.post : request.get; - return dispatcher(apiEndpoint + suffix, options); - } - }; - - const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => { - let query: any = parameters.query; - let id = query.albumId; - let suffix = 'albums'; - if (dispatch.required) { - if (!id) { - return undefined; - } - suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`; - } - return suffix; - }; - - const SuffixMap = new Map([ - [Album.Action.AddEnrichment, { required: true, method: "POST" }], - [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }], - [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }], - [Album.Action.Create, { required: false, method: "POST" }], - [Album.Action.Get, { required: true, ignore: true, method: "GET" }], - [Album.Action.List, { required: false, method: "GET" }], - [Album.Action.Share, { required: true, method: "POST" }], - [Album.Action.Unshare, { required: true, method: "POST" }] - ]); - -} diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index cd2a586eb..3b513aaf1 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -1,28 +1,122 @@ import request = require('request-promise'); -import { Authorization } from './GooglePhotosServerUtils'; +import { GoogleApiServerUtils } from './GoogleApiServerUtils'; +import * as fs from 'fs'; +import { Utils } from '../../../Utils'; +import * as path from 'path'; +import { Opt } from '../../../new_fields/Doc'; export namespace GooglePhotosUploadUtils { - interface UploadInformation { - title: string; - MEDIA_BINARY_DATA: string; + export interface Paths { + uploadDirectory: string; + credentialsPath: string; + tokenPath: string; } - const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads"; + export interface MediaInput { + description: string; + source: string; + } + + export interface DownloadInformation { + mediaPath: string; + contentType?: string; + contentSize?: string; + } + + const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`; + const headers = (type: string) => ({ + 'Content-Type': `application/${type}`, + 'Authorization': Bearer, + }); + + let Bearer: string; + let Paths: Paths; - export const SubmitUpload = async (parameters: Authorization & UploadInformation) => { - let options = { + export const initialize = async (paths: Paths) => { + Paths = paths; + const { tokenPath, credentialsPath } = paths; + const token = await GoogleApiServerUtils.RetrieveAccessToken({ tokenPath, credentialsPath }); + Bearer = `Bearer ${token}`; + }; + + export const DispatchGooglePhotosUpload = async (filename: string) => { + let body: Buffer; + if (filename.includes('upload_')) { + const mediaPath = Paths.uploadDirectory + filename; + body = await new Promise((resolve, reject) => { + fs.readFile(mediaPath, (error, data) => error ? reject(error) : resolve(data)); + }); + } else { + body = await request(filename, { encoding: null }); + } + const parameters = { + method: 'POST', headers: { - 'Content-Type': 'application/octet-stream', - Authorization: `Bearer ${parameters.token}`, - 'X-Goog-Upload-File-Name': parameters.title, + ...headers('octet-stream'), + 'X-Goog-Upload-File-Name': filename, 'X-Goog-Upload-Protocol': 'raw' }, - body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA }, - json: true + uri: prepend('uploads'), + body }; - const result = await request.post(apiEndpoint, options); - return result; + return new Promise(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body))); + }; + + export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => { + return new Promise((resolve, reject) => { + const parameters = { + method: 'POST', + headers: headers('json'), + uri: prepend('mediaItems:batchCreate'), + body: { newMediaItems } as any, + json: true + }; + album && (parameters.body.albumId = album.id); + request(parameters, (error, _response, body) => { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + }); }; + export namespace IOUtils { + + export const Download = async (url: string): Promise> => { + const filename = `temporary_upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + const temporaryDirectory = Paths.uploadDirectory + "temporary/"; + const mediaPath = temporaryDirectory + filename; + + if (!(await createIfNotExists(temporaryDirectory))) { + return undefined; + } + + return new Promise((resolve, reject) => { + request.head(url, (error, res) => { + if (error) { + return reject(error); + } + const information: DownloadInformation = { + mediaPath, + contentType: res.headers['content-type'], + contentSize: res.headers['content-length'], + }; + request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information)); + }); + }); + }; + + export const createIfNotExists = async (path: string) => { + if (await new Promise(resolve => fs.exists(path, resolve))) { + return true; + } + return new Promise(resolve => fs.mkdir(path, error => resolve(error === null))); + }; + + export const Destroy = (mediaPath: string) => new Promise(resolve => fs.unlink(mediaPath, error => resolve(error === null))); + } + } \ No newline at end of file diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts deleted file mode 100644 index f3025567d..000000000 --- a/src/server/apis/google/typings/albums.ts +++ /dev/null @@ -1,150 +0,0 @@ -export namespace Album { - - export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare); - - export enum Action { - AddEnrichment = "addEnrichment", - BatchAddMediaItems = "batchAddMediaItems", - BatchRemoveMediaItems = "batchRemoveMediaItems", - Create = "create", - Get = "get", - List = "list", - Share = "share", - Unshare = "unshare" - } - - export interface AddEnrichment { - action: Action.AddEnrichment; - albumId: string; - body: { - newEnrichmentItem: NewEnrichmentItem; - albumPosition: MediaRelativeAlbumPosition; - }; - } - - export interface BatchAddMediaItems { - action: Action.BatchAddMediaItems; - albumId: string; - body: { - mediaItemIds: string[]; - }; - } - - export interface BatchRemoveMediaItems { - action: Action.BatchRemoveMediaItems; - albumId: string; - body: { - mediaItemIds: string[]; - }; - } - - export interface Create { - action: Action.Create; - body: { - album: Template; - }; - } - - export interface Get { - action: Action.Get; - albumId: string; - } - - export interface List { - action: Action.List; - parameters: ListOptions; - } - - export interface ListOptions { - pageSize: number; - pageToken: string; - excludeNonAppCreatedData: boolean; - } - - export interface Share { - action: Action.Share; - albumId: string; - body: { - sharedAlbumOptions: SharedOptions; - }; - } - - export interface Unshare { - action: Action.Unshare; - albumId: string; - } - - export interface Template { - title: string; - } - - export interface Model { - id: string; - title: string; - productUrl: string; - isWriteable: boolean; - shareInfo: ShareInfo; - mediaItemsCount: string; - coverPhotoBaseUrl: string; - coverPhotoMediaItemId: string; - } - - export interface ShareInfo { - sharedAlbumOptions: SharedOptions; - shareableUrl: string; - shareToken: string; - isJoined: boolean; - isOwned: boolean; - } - - export interface SharedOptions { - isCollaborative: boolean; - isCommentable: boolean; - } - - export enum PositionType { - POSITION_TYPE_UNSPECIFIED, - FIRST_IN_ALBUM, - LAST_IN_ALBUM, - AFTER_MEDIA_ITEM, - AFTER_ENRICHMENT_ITEM - } - - export type Position = GeneralAlbumPosition | MediaRelativeAlbumPosition | EnrichmentRelativeAlbumPosition; - - interface GeneralAlbumPosition { - position: PositionType.FIRST_IN_ALBUM | PositionType.LAST_IN_ALBUM | PositionType.POSITION_TYPE_UNSPECIFIED; - } - - interface MediaRelativeAlbumPosition { - position: PositionType.AFTER_MEDIA_ITEM; - relativeMediaItemId: string; - } - - interface EnrichmentRelativeAlbumPosition { - position: PositionType.AFTER_ENRICHMENT_ITEM; - relativeEnrichmentItemId: string; - } - - export interface Location { - locationName: string; - latlng: { - latitude: number, - longitude: number - }; - } - - export interface NewEnrichmentItem { - textEnrichment: { - text: string; - }; - locationEnrichment: { - location: Location - }; - mapEnrichment: { - origin: { location: Location }, - destination: { location: Location } - }; - } - -} \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 2ac972ed8..f3c8cf82a 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glt5ByifP30HMz6a1fEG77qZlz9fBEOCz4PQ1VA8t_Ck2ZTPJKoyr6xc3-GFZISAqrrw2U8XpMyZv02_URfPwUX0Z_tMdlIFqsygowR-uClukbgQPNtgxp2LS1oW","refresh_token":"1/wK1cUVLnt581ba_pYGPdlTvAa-OS64nB5m5XOXEBJ8Q","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567556163894} \ No newline at end of file +{"access_token":"ya29.Glx7B9S6zCKDE0EgYk9xX9-RhcN8j4IwG9ONopTl1NkPX9FUOw0GI_81mY9bhaouuyOTnrc6FrZD5SDHolWwp3ABNT6l7TmhTLDILgGXIixZkWFRBPpF-xHC8lUd8A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567765465073} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 3940bbd58..fab00a02d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,7 +42,6 @@ var AdmZip = require('adm-zip'); import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; -import { GooglePhotos } from './apis/google/GooglePhotosServerUtils'; import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); @@ -418,10 +417,10 @@ app.get("/thumbnail/:filename", (req, res) => { let filename = req.params.filename; let noExt = filename.substring(0, filename.length - ".png".length); let pagenumber = parseInt(noExt.split('-')[1]); - fs.exists(uploadDir + filename, (exists: boolean) => { - console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`); + fs.exists(uploadDirectory + filename, (exists: boolean) => { + console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`); if (exists) { - let input = fs.createReadStream(uploadDir + filename); + let input = fs.createReadStream(uploadDirectory + filename); probe(input, (err: any, result: any) => { if (err) { console.log(err); @@ -432,7 +431,7 @@ app.get("/thumbnail/:filename", (req, res) => { }); } else { - LoadPage(uploadDir + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); + LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); } }); }); @@ -556,13 +555,13 @@ class NodeCanvasFactory { const pngTypes = [".png", ".PNG"]; const pdfTypes = [".pdf", ".PDF"]; const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"]; -const uploadDir = __dirname + "/public/files/"; +const uploadDirectory = __dirname + "/public/files/"; // SETTERS app.post( RouteStore.upload, (req, res) => { let form = new formidable.IncomingForm(); - form.uploadDir = uploadDir; + form.uploadDir = uploadDirectory; form.keepExtensions = true; // let path = req.body.path; console.log("upload"); @@ -592,7 +591,7 @@ app.post( } if (isImage) { resizers.forEach(resizer => { - fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext)); + fs.createReadStream(uploadDirectory + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + file.substring(0, file.length - ext.length) + resizer.suffix + ext)); }); } names.push(`/files/` + file); @@ -611,7 +610,7 @@ addSecureRoute( res.status(401).send("incorrect parameters specified"); return; } - imageDataUri.outputFile(uri, uploadDir + filename).then((savedName: string) => { + imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => { const ext = path.extname(savedName); let resizers = [ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, @@ -632,7 +631,7 @@ addSecureRoute( } if (isImage) { resizers.forEach(resizer => { - fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + filename + resizer.suffix + ext)); + fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext)); }); } res.send("/files/" + filename + ext); @@ -799,8 +798,8 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } } -const credentials = path.join(__dirname, "./credentials/google_docs_credentials.json"); -const token = path.join(__dirname, "./credentials/google_docs_token.json"); +const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json"); +const tokenPath = path.join(__dirname, "./credentials/google_docs_token.json"); const EndpointHandlerMap = new Map([ ["create", (api, params) => api.create(params)], @@ -811,7 +810,7 @@ const EndpointHandlerMap = new Map { let sector: any = req.params.sector; let action: any = req.params.action; - GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => { + GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, tokenPath }).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( @@ -825,36 +824,28 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.post(RouteStore.googlePhotosQuery, (req, res) => { - GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then( - token => { - GooglePhotos.ExecuteQuery({ token, query: req.body }) - .then(response => { - if (response === undefined) { - res.send("Error: unable to build suffix for Google Photos API request"); - return; - } - res.send(response); - }) - .catch(error => { - res.send(`Error: an exception occurred in the execution of this Google Photos API request\n${error}`); - }); - }, - error => res.send(error) - ); -}); +app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath }).then(token => res.send(token))); -app.post(RouteStore.googlePhotosMediaUpload, (req, res) => { - GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then( - token => { - GooglePhotosUploadUtils.SubmitUpload({ token, ...req.body }) - .then(response => { - res.send(response); - }).catch(error => { - res.send(`Error: an exception occurred in uploading the given media\n${error}`); - }); - }, - error => res.send(error)); +const tokenError = "Unable to successfully upload bytes for all images!"; +const mediaError = "Unable to convert all uploaded bytes to media items!"; + +app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { + const media: GooglePhotosUploadUtils.MediaInput[] = req.body.media; + await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); + const newMediaItems = await Promise.all(media.map(async element => { + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.source); + return !uploadToken ? undefined : { + description: element.description, + simpleMediaItem: { uploadToken } + }; + })); + if (!newMediaItems.every(item => item)) { + return res.send(tokenError); + } + GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( + success => res.send(success), + () => res.send(mediaError) + ); }); const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { -- cgit v1.2.3-70-g09d2 From d94509864920b2bbe7f4af8837f3af3f721b7dad Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 7 Sep 2019 13:19:10 -0400 Subject: implemented via context menu --- src/client/apis/google_docs/GooglePhotosClientUtils.ts | 4 ++-- src/client/views/MainView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 5 +++++ src/server/apis/google/GooglePhotosUploadUtils.ts | 16 ++++------------ src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 8 ++++---- tsconfig.json | 2 +- 7 files changed, 18 insertions(+), 21 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 2b72800a9..924362c03 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -13,8 +13,8 @@ export namespace GooglePhotosClientUtils { export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text())); export interface MediaInput { + url: string; description: string; - source: string; } export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => { @@ -29,7 +29,7 @@ export namespace GooglePhotosClientUtils { return undefined; } media.push({ - source: data.url.href, + url: data.url.href, description, } as MediaInput); }); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 6d366216e..7fe35494d 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -130,7 +130,7 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - this.executeGooglePhotosRoutine(); + // this.executeGooglePhotosRoutine(); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b60730a6b..b8a034efc 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,6 +41,8 @@ import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; +import { GooglePhotosClientUtils } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../../new_fields/URLField'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -588,6 +590,9 @@ export class DocumentView extends DocComponent(Docu subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" }); subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); + if (Cast(this.props.Document.data, ImageField)) { + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadMedia([this.props.Document]), icon: "caret-square-right" }); + } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 3b513aaf1..13db1df03 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -14,8 +14,8 @@ export namespace GooglePhotosUploadUtils { } export interface MediaInput { + url: string; description: string; - source: string; } export interface DownloadInformation { @@ -40,21 +40,13 @@ export namespace GooglePhotosUploadUtils { Bearer = `Bearer ${token}`; }; - export const DispatchGooglePhotosUpload = async (filename: string) => { - let body: Buffer; - if (filename.includes('upload_')) { - const mediaPath = Paths.uploadDirectory + filename; - body = await new Promise((resolve, reject) => { - fs.readFile(mediaPath, (error, data) => error ? reject(error) : resolve(data)); - }); - } else { - body = await request(filename, { encoding: null }); - } + export const DispatchGooglePhotosUpload = async (url: string) => { + const body = await request(url, { encoding: null }); const parameters = { method: 'POST', headers: { ...headers('octet-stream'), - 'X-Goog-Upload-File-Name': filename, + 'X-Goog-Upload-File-Name': path.basename(url), 'X-Goog-Upload-Protocol': 'raw' }, uri: prepend('uploads'), diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index f3c8cf82a..e67c4b5ba 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx7B9S6zCKDE0EgYk9xX9-RhcN8j4IwG9ONopTl1NkPX9FUOw0GI_81mY9bhaouuyOTnrc6FrZD5SDHolWwp3ABNT6l7TmhTLDILgGXIixZkWFRBPpF-xHC8lUd8A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567765465073} \ No newline at end of file +{"access_token":"ya29.Glx8B81Wqa67aMtB6AwlIUcLO4k0bnsICbtkXJUkqXWPIZgnSw0SnCG0jiFAmwLGPg8ca-Qk3R0SqWt4JlgwfrzuOqt90I0P8tHH2x_4RXfgisVBg4Muf8Gz59AEkA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567878663996} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index fab00a02d..99d8a02d4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -808,9 +808,9 @@ const EndpointHandlerMap = new Map { - let sector: any = req.params.sector; - let action: any = req.params.action; - GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, tokenPath }).then(endpoint => { + let sector: GoogleApiServerUtils.Service = req.params.sector; + let action: GoogleApiServerUtils.Action = req.params.action; + GoogleApiServerUtils.GetEndpoint(sector, { credentialsPath, tokenPath }).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( @@ -833,7 +833,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { const media: GooglePhotosUploadUtils.MediaInput[] = req.body.media; await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); const newMediaItems = await Promise.all(media.map(async element => { - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.source); + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); return !uploadToken ? undefined : { description: element.description, simpleMediaItem: { uploadToken } diff --git a/tsconfig.json b/tsconfig.json index 9ea91ec49..75541abca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "lib": [ "dom", "es2015" - ], + ] }, // "exclude": [ // "node_modules", -- cgit v1.2.3-70-g09d2 From 32cd51e2bcc0a8cf498c0b31a5ead60802f672de Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 7 Sep 2019 17:08:49 -0400 Subject: working on import from google photos --- .../apis/google_docs/GooglePhotosClientUtils.ts | 123 +++++++++++++++++++-- src/client/views/MainView.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/server/RouteStore.ts | 3 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 14 +-- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 16 ++- 7 files changed, 140 insertions(+), 24 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 924362c03..e8daf3dd4 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -6,9 +6,42 @@ import { Doc, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import requestImageSize = require('../../util/request-image-size'); import Photos = require('googlephotos'); +import { RichTextField } from "../../../new_fields/RichTextField"; +import { RichTextUtils } from "../../../new_fields/RichTextUtils"; +import { EditorState } from "prosemirror-state"; +import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; export namespace GooglePhotosClientUtils { + export enum ContentCategories { + NONE = 'NONE', + LANDSCAPES = 'LANDSCAPES', + RECEIPTS = 'RECEIPTS', + CITYSCAPES = 'CITYSCAPES', + LANDMARKS = 'LANDMARKS', + SELFIES = 'SELFIES', + PEOPLE = 'PEOPLE', + PETS = 'PETS', + WEDDINGS = 'WEDDINGS', + BIRTHDAYS = 'BIRTHDAYS', + DOCUMENTS = 'DOCUMENTS', + TRAVEL = 'TRAVEL', + ANIMALS = 'ANIMALS', + FOOD = 'FOOD', + SPORT = 'SPORT', + NIGHT = 'NIGHT', + PERFORMANCES = 'PERFORMANCES', + WHITEBOARDS = 'WHITEBOARDS', + SCREENSHOTS = 'SCREENSHOTS', + UTILITY = 'UTILITY' + } + + export enum MediaType { + ALL_MEDIA = 'ALL_MEDIA', + PHOTO = 'PHOTO', + VIDEO = 'VIDEO' + } + export type AlbumReference = { id: string } | { title: string }; export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text())); @@ -17,26 +50,96 @@ export namespace GooglePhotosClientUtils { description: string; } - export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => { + export const UploadImageDocuments = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { if (album && "title" in album) { - album = (await endpoint()).albums.create(album.title); + album = await (await endpoint()).albums.create(album.title); } const media: MediaInput[] = []; sources.forEach(document => { const data = Cast(Doc.GetProto(document).data, ImageField); - const description = StrCast(document.caption); - if (!data) { - return undefined; - } - media.push({ + data && media.push({ url: data.url.href, - description, - } as MediaInput); + description: parseDescription(document, descriptionKey), + }); }); if (media.length) { return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); } - return undefined; + }; + + const parseDescription = (document: Doc, descriptionKey: string) => { + let description: string = Utils.prepend("/doc/" + document[Id]); + const target = document[descriptionKey]; + if (typeof target === "string") { + description = target; + } else if (target instanceof RichTextField) { + description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data))); + } + return description; + }; + + export interface DateRange { + after: Date; + before: Date; + } + export interface SearchOptions { + pageSize: number; + included: ContentCategories[]; + excluded: ContentCategories[]; + date: Opt; + includeArchivedMedia: boolean; + type: MediaType; + } + + const DefaultSearchOptions: SearchOptions = { + pageSize: 20, + included: [], + excluded: [], + date: undefined, + includeArchivedMedia: true, + type: MediaType.ALL_MEDIA + }; + + export interface SearchResponse { + mediaItems: any[]; + nextPageToken: string; + } + + export const Search = async (requested: Opt>) => { + const options = requested || DefaultSearchOptions; + const photos = await endpoint(); + const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); + + const included = options.included || []; + const excluded = options.excluded || []; + const contentFilter = new photos.ContentFilter(); + included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category)); + excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); + filters.setContentFilter(contentFilter); + + const date = options.date; + if (date) { + const dateFilter = new photos.DateFilter(); + if (date instanceof Date) { + dateFilter.addDate(date); + } else { + dateFilter.addRange(date.after, date.before); + } + filters.setDateFilter(dateFilter); + } + + filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); + + return new Promise((resolve, reject) => { + photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => { + if (!response) { + return reject(); + } + let filenames = await PostToServer(RouteStore.googlePhotosMediaDownload, response); + console.log(filenames); + resolve(filenames); + }); + }); }; } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7fe35494d..ee58c684a 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -131,6 +131,8 @@ export class MainView extends React.Component { window.addEventListener("keydown", KeyManager.Instance.handle); // this.executeGooglePhotosRoutine(); + const imageTag = GooglePhotosClientUtils.ContentCategories; + GooglePhotosClientUtils.Search({ included: [imageTag.ANIMALS] }); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -155,7 +157,7 @@ export class MainView extends React.Component { doc.caption = "Well isn't this a nice cat image!"; let photos = await GooglePhotosClientUtils.endpoint(); let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; - console.log(await GooglePhotosClientUtils.UploadMedia([doc], { id: albumId })); + console.log(await GooglePhotosClientUtils.UploadImageDocuments([doc], { id: albumId })); } componentWillUnMount() { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b8a034efc..4033ffd9c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -591,7 +591,7 @@ export class DocumentView extends DocComponent(Docu subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); if (Cast(this.props.Document.data, ImageField)) { - cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadMedia([this.props.Document]), icon: "caret-square-right" }); + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImageDocuments([this.props.Document]), icon: "caret-square-right" }); } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index b221b71bc..f65e6134c 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -33,6 +33,7 @@ export enum RouteStore { cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", googlePhotosAccessToken = "/googlePhotosAccessToken", - googlePhotosMediaUpload = "/googlePhotosMediaUpload" + googlePhotosMediaUpload = "/googlePhotosMediaUpload", + googlePhotosMediaDownload = "/googlePhotosMediaDownload" } \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 13db1df03..032bc2a2d 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -20,6 +20,7 @@ export namespace GooglePhotosUploadUtils { export interface DownloadInformation { mediaPath: string; + fileName: string; contentType?: string; contentSize?: string; } @@ -77,15 +78,9 @@ export namespace GooglePhotosUploadUtils { export namespace IOUtils { - export const Download = async (url: string): Promise> => { - const filename = `temporary_upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; - const temporaryDirectory = Paths.uploadDirectory + "temporary/"; - const mediaPath = temporaryDirectory + filename; - - if (!(await createIfNotExists(temporaryDirectory))) { - return undefined; - } - + export const Download = async (url: string, filename?: string): Promise> => { + const resolved = filename || `upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + const mediaPath = Paths.uploadDirectory + resolved; return new Promise((resolve, reject) => { request.head(url, (error, res) => { if (error) { @@ -95,6 +90,7 @@ export namespace GooglePhotosUploadUtils { mediaPath, contentType: res.headers['content-type'], contentSize: res.headers['content-length'], + fileName: resolved }; request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information)); }); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index e67c4b5ba..88838e18a 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx8B81Wqa67aMtB6AwlIUcLO4k0bnsICbtkXJUkqXWPIZgnSw0SnCG0jiFAmwLGPg8ca-Qk3R0SqWt4JlgwfrzuOqt90I0P8tHH2x_4RXfgisVBg4Muf8Gz59AEkA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567878663996} \ No newline at end of file +{"access_token":"ya29.Glx8B266dydsOIEYhedUZYQ8sIsR9utSSxCBUex0O85zYrujZCSTbjVhrXF3Y4q41mLFghLwspgW-1w6zqnGnMtkZhuDGpBGArIwLZsJDyhUugEu3xvh7gY78WfePA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567890805451} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 99d8a02d4..aadadb11a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -113,7 +113,7 @@ function addSecureRoute(method: Method, ) { let abstracted = (req: express.Request, res: express.Response) => { if (req.user) { - handler(req.user as any, res, req); + handler(req.user, res, req); } else { req.session!.target = req.originalUrl; onRejection(res, req); @@ -848,6 +848,20 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { ); }); +app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { + const contents = req.body; + if (!contents) { + return res.send(undefined); + } + await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); + let bundles: GooglePhotosUploadUtils.DownloadInformation[] = []; + await Promise.all(contents.mediaItems.forEach(async (item: any) => { + const information = await GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, item.filename); + information && bundles.push(information); + })); + res.send(bundles); +}); + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", -- cgit v1.2.3-70-g09d2 From c24f5f29dff8dd22f1d4029a2722ee4d1a725aad Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 7 Sep 2019 23:15:32 -0400 Subject: collection of search --- src/client/apis/google_docs/GooglePhotosClientUtils.ts | 14 +++++++------- src/client/views/MainView.tsx | 7 ++++--- src/server/apis/google/GooglePhotosUploadUtils.ts | 10 +++++----- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 10 ++++------ 5 files changed, 21 insertions(+), 22 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index e8daf3dd4..bb5d23971 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -10,6 +10,7 @@ import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from "../../../new_fields/RichTextUtils"; import { EditorState } from "prosemirror-state"; import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; +import { Docs } from "../../documents/Documents"; export namespace GooglePhotosClientUtils { @@ -130,14 +131,13 @@ export namespace GooglePhotosClientUtils { filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); - return new Promise((resolve, reject) => { + return new Promise(resolve => { photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => { - if (!response) { - return reject(); - } - let filenames = await PostToServer(RouteStore.googlePhotosMediaDownload, response); - console.log(filenames); - resolve(filenames); + response && resolve(Docs.Create.StackingDocument((await PostToServer(RouteStore.googlePhotosMediaDownload, response)).map((download: any) => { + let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileName}`)); + document.contentSize = download.contentSize; + return document; + }), { width: 500, height: 500 })); }); }); }; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ee58c684a..b72df3715 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -131,8 +131,6 @@ export class MainView extends React.Component { window.addEventListener("keydown", KeyManager.Instance.handle); // this.executeGooglePhotosRoutine(); - const imageTag = GooglePhotosClientUtils.ContentCategories; - GooglePhotosClientUtils.Search({ included: [imageTag.ANIMALS] }); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -472,12 +470,15 @@ export class MainView extends React.Component { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); - let btns: [React.RefObject, IconName, string, () => Doc][] = [ + let googlePhotosSearch = () => GooglePhotosClientUtils.Search({ included: [GooglePhotosClientUtils.ContentCategories.ANIMALS] }); + + let btns: [React.RefObject, IconName, string, () => Doc | Promise][] = [ [React.createRef(), "object-group", "Add Collection", addColNode], [React.createRef(), "tv", "Add Presentation Trail", addPresNode], [React.createRef(), "globe-asia", "Add Website", addWebNode], [React.createRef(), "bolt", "Add Button", addButtonDocument], [React.createRef(), "file", "Add Document Dragger", addDragboxNode], + [React.createRef(), "object-group", "Test Google Photos Search", googlePhotosSearch], [React.createRef(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode //[React.createRef(), "play", "Add Youtube Searcher", addYoutubeSearcher], ]; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 032bc2a2d..9b3e68761 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -78,8 +78,8 @@ export namespace GooglePhotosUploadUtils { export namespace IOUtils { - export const Download = async (url: string, filename?: string): Promise> => { - const resolved = filename || `upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + export const Download = async (url: string, filename?: string, prefix = ""): Promise> => { + const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; const mediaPath = Paths.uploadDirectory + resolved; return new Promise((resolve, reject) => { request.head(url, (error, res) => { @@ -87,10 +87,10 @@ export namespace GooglePhotosUploadUtils { return reject(error); } const information: DownloadInformation = { - mediaPath, - contentType: res.headers['content-type'], + fileName: resolved, contentSize: res.headers['content-length'], - fileName: resolved + contentType: res.headers['content-type'], + mediaPath }; request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information)); }); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 88838e18a..a1c23ea35 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx8B266dydsOIEYhedUZYQ8sIsR9utSSxCBUex0O85zYrujZCSTbjVhrXF3Y4q41mLFghLwspgW-1w6zqnGnMtkZhuDGpBGArIwLZsJDyhUugEu3xvh7gY78WfePA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567890805451} \ No newline at end of file +{"access_token":"ya29.Glx9B3Fumh3qHpgasQvHNNrwNXtmTVWJR9XckFsnUjOswDOO91ccF3FhD4ko7Z-3rvxEljpP1Qj5BgNq305pt-pgIquoLPWYiaEtinHNF7IXGPz4s4raqJWEJPJxow","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567913435149} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index aadadb11a..49010e7e2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -848,18 +848,16 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { ); }); +const prefix = "google_photos_"; app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { const contents = req.body; if (!contents) { return res.send(undefined); } await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); - let bundles: GooglePhotosUploadUtils.DownloadInformation[] = []; - await Promise.all(contents.mediaItems.forEach(async (item: any) => { - const information = await GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, item.filename); - information && bundles.push(information); - })); - res.send(bundles); + res.send(await Promise.all(contents.mediaItems.map((item: any) => + GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, undefined, prefix))) + ); }); const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { -- cgit v1.2.3-70-g09d2 From f18e2265e5d468f1cbf6e82dd5f01d5f5216b851 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 8 Sep 2019 04:16:04 -0400 Subject: factored out collection creation, sharp() resizers and image management --- .../apis/google_docs/GooglePhotosClientUtils.ts | 27 +++-- src/client/documents/Documents.ts | 9 +- src/client/views/MainView.tsx | 2 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 119 +++++++++++++++------ src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 27 +++-- 6 files changed, 131 insertions(+), 55 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index bb5d23971..5f5b39b14 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,16 +1,16 @@ import { PostToServer, Utils } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { ImageField } from "../../../new_fields/URLField"; -import { StrCast, Cast } from "../../../new_fields/Types"; +import { Cast } from "../../../new_fields/Types"; import { Doc, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import requestImageSize = require('../../util/request-image-size'); import Photos = require('googlephotos'); import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from "../../../new_fields/RichTextUtils"; import { EditorState } from "prosemirror-state"; import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; -import { Docs } from "../../documents/Documents"; +import { Docs, DocumentOptions } from "../../documents/Documents"; +import { type } from "os"; export namespace GooglePhotosClientUtils { @@ -98,7 +98,7 @@ export namespace GooglePhotosClientUtils { excluded: [], date: undefined, includeArchivedMedia: true, - type: MediaType.ALL_MEDIA + type: MediaType.ALL_MEDIA, }; export interface SearchResponse { @@ -106,7 +106,18 @@ export namespace GooglePhotosClientUtils { nextPageToken: string; } - export const Search = async (requested: Opt>) => { + export type CollectionConstructor = (data: Array, options: DocumentOptions, ...args: any) => Doc; + export const CollectionFromSearch = async (provider: CollectionConstructor, requested: Opt>): Promise => { + let downloads = await Search(requested); + return provider(downloads.map((download: any) => { + let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileNames.clean}`)); + document.fillColumn = true; + document.contentSize = download.contentSize; + return document; + }), { width: 500, height: 500 }); + }; + + export const Search = async (requested: Opt>): Promise => { const options = requested || DefaultSearchOptions; const photos = await endpoint(); const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); @@ -133,11 +144,7 @@ export namespace GooglePhotosClientUtils { return new Promise(resolve => { photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => { - response && resolve(Docs.Create.StackingDocument((await PostToServer(RouteStore.googlePhotosMediaDownload, response)).map((download: any) => { - let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileName}`)); - document.contentSize = download.contentSize; - return document; - }), { width: 500, height: 500 })); + response && resolve(await PostToServer(RouteStore.googlePhotosMediaDownload, response)); }); }); }; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4b7f1eeb6..9bac57d16 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -21,7 +21,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; -import { OmitKeys, JSONUtils } from "../../Utils"; +import { OmitKeys, JSONUtils, Utils } from "../../Utils"; import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; @@ -332,7 +332,12 @@ export namespace Docs { export function ImageDocument(url: string, options: DocumentOptions = {}) { let imgField = new ImageField(new URL(url)); let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options }); - requestImageSize(imgField.url.href) + let target = imgField.url.href; + if (new RegExp(window.location.origin).test(target)) { + let extension = path.extname(target); + target = `${target.substring(0, target.length - extension.length)}_o${extension}`; + } + requestImageSize(target) .then((size: any) => { let aspect = size.height / size.width; if (!inst.proto!.nativeWidth) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b72df3715..326c13424 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -470,7 +470,7 @@ export class MainView extends React.Component { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); - let googlePhotosSearch = () => GooglePhotosClientUtils.Search({ included: [GooglePhotosClientUtils.ContentCategories.ANIMALS] }); + let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); let btns: [React.RefObject, IconName, string, () => Doc | Promise][] = [ [React.createRef(), "object-group", "Add Collection", addColNode], diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 9b3e68761..5ac3eaef7 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -4,6 +4,9 @@ import * as fs from 'fs'; import { Utils } from '../../../Utils'; import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; +import * as sharp from 'sharp'; + +const uploadDirectory = path.join(__dirname, "../../public/files/"); export namespace GooglePhotosUploadUtils { @@ -18,13 +21,6 @@ export namespace GooglePhotosUploadUtils { description: string; } - export interface DownloadInformation { - mediaPath: string; - fileName: string; - contentType?: string; - contentSize?: string; - } - const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`; const headers = (type: string) => ({ 'Content-Type': `application/${type}`, @@ -76,35 +72,92 @@ export namespace GooglePhotosUploadUtils { }); }; - export namespace IOUtils { +} - export const Download = async (url: string, filename?: string, prefix = ""): Promise> => { - const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; - const mediaPath = Paths.uploadDirectory + resolved; - return new Promise((resolve, reject) => { - request.head(url, (error, res) => { - if (error) { - return reject(error); - } - const information: DownloadInformation = { - fileName: resolved, - contentSize: res.headers['content-length'], - contentType: res.headers['content-type'], - mediaPath - }; - request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information)); - }); - }); - }; +export namespace DownloadUtils { - export const createIfNotExists = async (path: string) => { - if (await new Promise(resolve => fs.exists(path, resolve))) { - return true; - } - return new Promise(resolve => fs.mkdir(path, error => resolve(error === null))); - }; + export interface Size { + width: number; + suffix: string; + } - export const Destroy = (mediaPath: string) => new Promise(resolve => fs.unlink(mediaPath, error => resolve(error === null))); + export const Sizes: { [size: string]: Size } = { + SMALL: { width: 100, suffix: "_s" }, + MEDIUM: { width: 400, suffix: "_m" }, + LARGE: { width: 900, suffix: "_l" }, + }; + + const png = ".png"; + const pngs = [".png", ".PNG"]; + const jpg = [".jpg", ".JPG", ".jpeg", ".JPEG"]; + const size = "content-length"; + const type = "content-type"; + + export interface DownloadInformation { + mediaPaths: string[]; + fileNames: { [key: string]: string }; + contentSize?: string; + contentType?: string; } + const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + const sanitize = (filename: string) => filename.replace(/\s+/g, "_"); + + export const Download = async (url: string, filename?: string, prefix = ""): Promise> => { + const resolved = filename ? sanitize(filename) : generate(prefix, url); + const extension = path.extname(url) || path.extname(resolved) || png; + return new Promise((resolve, reject) => { + request.head(url, async (error, res) => { + if (error) { + return reject(error); + } + const information: DownloadInformation = { + fileNames: { clean: resolved }, + contentSize: res.headers[size], + contentType: res.headers[type], + mediaPaths: [] + }; + const resizers = [ + { resizer: sharp().rotate(), suffix: "_o" }, + ...Object.values(Sizes).map(size => ({ + resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(), + suffix: size.suffix + })) + ]; + let validated = true; + if (pngs.includes(extension)) { + resizers.forEach(element => element.resizer = element.resizer.png()); + } else if (jpg.includes(extension)) { + resizers.forEach(element => element.resizer = element.resizer.jpeg()); + } else { + validated = false; + } + if (validated) { + for (let resizer of resizers) { + const suffix = resizer.suffix; + let mediaPath: string; + await new Promise(resolve => { + const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; + information.mediaPaths.push(mediaPath = uploadDirectory + filename); + information.fileNames[suffix] = filename; + request(url) + .pipe(resizer.resizer) + .pipe(fs.createWriteStream(mediaPath)) + .on('close', resolve); + }); + } + resolve(information); + } + }); + }); + }; + + export const createIfNotExists = async (path: string) => { + if (await new Promise(resolve => fs.exists(path, resolve))) { + return true; + } + return new Promise(resolve => fs.mkdir(path, error => resolve(error === null))); + }; + + export const Destroy = (mediaPath: string) => new Promise(resolve => fs.unlink(mediaPath, error => resolve(error === null))); } \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index a1c23ea35..4f911f7e0 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx9B3Fumh3qHpgasQvHNNrwNXtmTVWJR9XckFsnUjOswDOO91ccF3FhD4ko7Z-3rvxEljpP1Qj5BgNq305pt-pgIquoLPWYiaEtinHNF7IXGPz4s4raqJWEJPJxow","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567913435149} \ No newline at end of file +{"access_token":"ya29.Glx9B3_l5JbKNtvzx378Nsz917bP-OTKf6VZzc2K8QDBm-Y0j_-c8v7bL8LCEM3wF8d7JauF-5Z4Uq4v7wPwUQUlDO1uPyoHeSF6iz98xkgJr9OW4KzJo2Ij722gpQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567931641928} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 49010e7e2..013345a76 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,7 +42,7 @@ 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 { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils'; +import { GooglePhotosUploadUtils, DownloadUtils } from './apis/google/GooglePhotosUploadUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -154,6 +154,11 @@ app.get("/buxton", (req, res) => { command_line('python scraper.py', cwd).then(onResolved, tryPython3); }); +const STATUS = { + OK: 200, + BAD_REQUEST: 400 +}; + const command_line = (command: string, fromDirectory?: string) => { return new Promise((resolve, reject) => { let options: ExecOptions = {}; @@ -848,16 +853,22 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { ); }); +interface MediaItem { + baseUrl: string; + filename: string; +} const prefix = "google_photos_"; + app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { - const contents = req.body; - if (!contents) { - return res.send(undefined); + const contents: { mediaItems: MediaItem[] } = req.body; + if (contents) { + const downloads = contents.mediaItems.map(item => + DownloadUtils.Download(item.baseUrl, item.filename, prefix) + ); + res.status(STATUS.OK).send(await Promise.all(downloads)); + return; } - await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); - res.send(await Promise.all(contents.mediaItems.map((item: any) => - GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, undefined, prefix))) - ); + res.status(STATUS.BAD_REQUEST).send(); }); const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { -- cgit v1.2.3-70-g09d2 From 65e5366f59ef2933460aafdc98790f42611f149f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 8 Sep 2019 15:24:53 -0400 Subject: refactor of uploader to handle local and remote images --- .../util/Import & Export/DirectoryImportBox.tsx | 2 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 101 ++++++++++++--------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 68 ++++++-------- 4 files changed, 89 insertions(+), 84 deletions(-) (limited to 'src/server') diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 75b0b52a7..35d6e3c60 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -313,7 +313,7 @@ export default class DirectoryImportBox extends React.Component style={{ pointerEvents: "none", position: "absolute", - right: isEditing ? 16.3 : 14.5, + right: isEditing ? 14 : 15, top: isEditing ? 15.4 : 16, opacity: uploading ? 0 : 1, transition: "0.4s opacity ease" diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 5ac3eaef7..d4a2a2bb3 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -93,65 +93,78 @@ export namespace DownloadUtils { const size = "content-length"; const type = "content-type"; - export interface DownloadInformation { + export interface UploadInformation { mediaPaths: string[]; fileNames: { [key: string]: string }; - contentSize?: string; + contentSize?: number; contentType?: string; } const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; const sanitize = (filename: string) => filename.replace(/\s+/g, "_"); - export const Download = async (url: string, filename?: string, prefix = ""): Promise> => { + export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise> => { const resolved = filename ? sanitize(filename) : generate(prefix, url); const extension = path.extname(url) || path.extname(resolved) || png; - return new Promise((resolve, reject) => { - request.head(url, async (error, res) => { - if (error) { - return reject(error); - } - const information: DownloadInformation = { - fileNames: { clean: resolved }, - contentSize: res.headers[size], - contentType: res.headers[type], - mediaPaths: [] - }; - const resizers = [ - { resizer: sharp().rotate(), suffix: "_o" }, - ...Object.values(Sizes).map(size => ({ - resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(), - suffix: size.suffix - })) - ]; - let validated = true; - if (pngs.includes(extension)) { - resizers.forEach(element => element.resizer = element.resizer.png()); - } else if (jpg.includes(extension)) { - resizers.forEach(element => element.resizer = element.resizer.jpeg()); - } else { - validated = false; - } - if (validated) { - for (let resizer of resizers) { - const suffix = resizer.suffix; - let mediaPath: string; - await new Promise(resolve => { - const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; - information.mediaPaths.push(mediaPath = uploadDirectory + filename); - information.fileNames[suffix] = filename; - request(url) - .pipe(resizer.resizer) - .pipe(fs.createWriteStream(mediaPath)) - .on('close', resolve); - }); + let information: UploadInformation = { + mediaPaths: [], + fileNames: { clean: resolved } + }; + const { isLocal, stream } = classify(url = path.normalize(url)); + if (!isLocal) { + const metadata = (await new Promise((resolve, reject) => { + request.head(url, async (error, res) => { + if (error) { + return reject(error); } - resolve(information); + resolve(res); + }); + })).headers; + information.contentSize = parseInt(metadata[size]); + information.contentType = metadata[type]; + } + return new Promise(async (resolve, reject) => { + const resizers = [ + { resizer: sharp().rotate(), suffix: "_o" }, + ...Object.values(Sizes).map(size => ({ + resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(), + suffix: size.suffix + })) + ]; + let validated = true; + if (pngs.includes(extension)) { + resizers.forEach(element => element.resizer = element.resizer.png()); + } else if (jpg.includes(extension)) { + resizers.forEach(element => element.resizer = element.resizer.jpeg()); + } else { + validated = false; + } + if (validated) { + for (let resizer of resizers) { + const suffix = resizer.suffix; + let mediaPath: string; + await new Promise(resolve => { + const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; + information.mediaPaths.push(mediaPath = uploadDirectory + filename); + information.fileNames[suffix] = filename; + stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) + .on('close', resolve) + .on('error', reject); + }); } - }); + resolve(information); + } }); }; + const classify = (url: string) => { + const isLocal = /Dash-Web\\src\\server\\public\\files/g.test(url); + return { + isLocal, + stream: isLocal ? fs.createReadStream : request + }; + }; + export const createIfNotExists = async (path: string) => { if (await new Promise(resolve => fs.exists(path, resolve))) { return true; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 4f911f7e0..66668aaef 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx9B3_l5JbKNtvzx378Nsz917bP-OTKf6VZzc2K8QDBm-Y0j_-c8v7bL8LCEM3wF8d7JauF-5Z4Uq4v7wPwUQUlDO1uPyoHeSF6iz98xkgJr9OW4KzJo2Ij722gpQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567931641928} \ No newline at end of file +{"access_token":"ya29.Glx9BwKT8sxbXNR78f2Ks3pAe2DfsxOhrYMj8SACNi13xwJ0MtLU4WYb4_cbHAj2X8imZW9eUBlAsY9RXoMEPOmVpMlhMjxVZKBo_0lwJ6xSTunSdrR1e8P7vkRV4Q","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567962258021} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 013345a76..54525cd31 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,7 +42,7 @@ 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 { GooglePhotosUploadUtils, DownloadUtils } from './apis/google/GooglePhotosUploadUtils'; +import { GooglePhotosUploadUtils, DownloadUtils as UploadUtils } from './apis/google/GooglePhotosUploadUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -156,7 +156,8 @@ app.get("/buxton", (req, res) => { const STATUS = { OK: 200, - BAD_REQUEST: 400 + BAD_REQUEST: 400, + EXECUTION_ERROR: 500 }; const command_line = (command: string, fromDirectory?: string) => { @@ -558,7 +559,6 @@ class NodeCanvasFactory { } const pngTypes = [".png", ".PNG"]; -const pdfTypes = [".pdf", ".PDF"]; const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"]; const uploadDirectory = __dirname + "/public/files/"; // SETTERS @@ -568,37 +568,11 @@ app.post( let form = new formidable.IncomingForm(); form.uploadDir = uploadDirectory; form.keepExtensions = true; - // let path = req.body.path; - console.log("upload"); - form.parse(req, (err, fields, files) => { - console.log("parsing"); + form.parse(req, async (_err, _fields, files) => { let names: string[] = []; for (const name in files) { const file = path.basename(files[name].path); - const ext = path.extname(file); - let resizers = [ - { resizer: sharp().rotate(), suffix: "_o" }, - { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }).rotate(), suffix: "_s" }, - { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }).rotate(), suffix: "_m" }, - { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }).rotate(), suffix: "_l" }, - ]; - let isImage = false; - if (pngTypes.includes(ext)) { - resizers.forEach(element => { - element.resizer = element.resizer.png(); - }); - isImage = true; - } else if (jpgTypes.includes(ext)) { - resizers.forEach(element => { - element.resizer = element.resizer.jpeg(); - }); - isImage = true; - } - if (isImage) { - resizers.forEach(resizer => { - fs.createReadStream(uploadDirectory + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + file.substring(0, file.length - ext.length) + resizer.suffix + ext)); - }); - } + await UploadUtils.UploadImage(uploadDirectory + file, file); names.push(`/files/` + file); } res.send(names); @@ -845,11 +819,11 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { }; })); if (!newMediaItems.every(item => item)) { - return res.send(tokenError); + return res.status(STATUS.EXECUTION_ERROR).send(tokenError); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - success => res.send(success), - () => res.send(mediaError) + success => res.status(STATUS.OK).send(success), + () => res.status(STATUS.EXECUTION_ERROR).send(mediaError) ); }); @@ -859,18 +833,36 @@ interface MediaItem { } const prefix = "google_photos_"; +const downloadError = "Encountered an error while executing downloads."; +const requestError = "Unable to execute download: the body's media items were malformed."; + app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { const contents: { mediaItems: MediaItem[] } = req.body; if (contents) { - const downloads = contents.mediaItems.map(item => - DownloadUtils.Download(item.baseUrl, item.filename, prefix) + const pending = contents.mediaItems.map(item => + UploadUtils.UploadImage(item.baseUrl, item.filename, prefix) ); - res.status(STATUS.OK).send(await Promise.all(downloads)); + const completed = await Promise.all(pending).catch(error => _error(res, downloadError, error)); + _success(res, completed); return; } - res.status(STATUS.BAD_REQUEST).send(); + _invalid(res, requestError); }); +const _error = (res: Response, message: string, error: any) => { + res.statusMessage = message; + res.status(STATUS.EXECUTION_ERROR).send(error); +}; + +const _success = (res: Response, body: any) => { + res.status(STATUS.OK).send(body); +}; + +const _invalid = (res: Response, message: string) => { + res.statusMessage = message; + res.status(STATUS.BAD_REQUEST).send(); +}; + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", -- cgit v1.2.3-70-g09d2 From 0c987a119fc6baa344cd6a8d229556c02af64898 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 9 Sep 2019 11:21:00 -0400 Subject: updates --- .../util/Import & Export/DirectoryImportBox.tsx | 48 ++++++++++++---------- src/server/apis/google/GooglePhotosUploadUtils.ts | 44 ++++++++++---------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 22 ++++++---- 4 files changed, 64 insertions(+), 52 deletions(-) (limited to 'src/server') diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index ab2801ee3..7693a388f 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -18,8 +18,14 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; +import { GooglePhotosClientUtils } from "../../apis/google_docs/GooglePhotosClientUtils"; const unsupported = ["text/html", "text/plain"]; +interface FileResponse { + name: string; + path: string; + type: string; +} @observer export default class DirectoryImportBox extends React.Component { @@ -87,34 +93,32 @@ export default class DirectoryImportBox extends React.Component let sizes = []; let modifiedDates = []; + let formData = new FormData(); for (let uploaded_file of validated) { - let formData = new FormData(); - formData.append('file', uploaded_file); - let dropFileName = uploaded_file ? uploaded_file.name : "-empty-"; - let type = uploaded_file.type; - + formData.append(Utils.GenerateGuid(), uploaded_file); sizes.push(uploaded_file.size); modifiedDates.push(uploaded_file.lastModified); - runInAction(() => this.remaining++); - - let prom = fetch(Utils.prepend(RouteStore.upload), { - method: 'POST', - body: formData - }).then(async (res: Response) => { - let names = await res.json(); - console.log(names); - await Promise.all(names.map((file: any) => { - let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); - docPromise.then(doc => { - doc && docs.push(doc) && runInAction(() => this.remaining--); - }); - })); - }); - promises.push(prom); } - await Promise.all(promises); + const parameters = { method: 'POST', body: formData }; + const uploads: FileResponse[] = await (await fetch(Utils.prepend(RouteStore.upload), parameters)).json(); + + await Promise.all(uploads.map(async upload => { + const type = upload.type; + const path = Utils.prepend(upload.path); + const options = { + nativeWidth: 300, + width: 300, + title: upload.name + }; + const document = await Docs.Get.DocumentFromType(type, path, options); + document && docs.push(document) && runInAction(() => this.remaining--); + console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`); + })); + + await GooglePhotosClientUtils.UploadImageDocuments(docs, { title: directory }); + console.log("Finished upload!"); for (let i = 0; i < docs.length; i++) { let doc = docs[i]; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index d4a2a2bb3..35f986250 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -89,7 +89,8 @@ export namespace DownloadUtils { const png = ".png"; const pngs = [".png", ".PNG"]; - const jpg = [".jpg", ".JPG", ".jpeg", ".JPEG"]; + const jpgs = [".jpg", ".JPG", ".jpeg", ".JPEG"]; + const formats = [".jpg", ".png", ".gif"]; const size = "content-length"; const type = "content-type"; @@ -110,7 +111,8 @@ export namespace DownloadUtils { mediaPaths: [], fileNames: { clean: resolved } }; - const { isLocal, stream } = classify(url = path.normalize(url)); + const { isLocal, stream, normalized } = classify(url); + url = normalized; if (!isLocal) { const metadata = (await new Promise((resolve, reject) => { request.head(url, async (error, res) => { @@ -131,37 +133,35 @@ export namespace DownloadUtils { suffix: size.suffix })) ]; - let validated = true; if (pngs.includes(extension)) { resizers.forEach(element => element.resizer = element.resizer.png()); - } else if (jpg.includes(extension)) { + } else if (jpgs.includes(extension)) { resizers.forEach(element => element.resizer = element.resizer.jpeg()); - } else { - validated = false; + } else if (!formats.includes(extension.toLowerCase())) { + return reject(); } - if (validated) { - for (let resizer of resizers) { - const suffix = resizer.suffix; - let mediaPath: string; - await new Promise(resolve => { - const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; - information.mediaPaths.push(mediaPath = uploadDirectory + filename); - information.fileNames[suffix] = filename; - stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) - .on('close', resolve) - .on('error', reject); - }); - } - resolve(information); + for (let resizer of resizers) { + const suffix = resizer.suffix; + let mediaPath: string; + await new Promise(resolve => { + const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; + information.mediaPaths.push(mediaPath = uploadDirectory + filename); + information.fileNames[suffix] = filename; + stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) + .on('close', resolve) + .on('error', reject); + }); } + resolve(information); }); }; const classify = (url: string) => { - const isLocal = /Dash-Web\\src\\server\\public\\files/g.test(url); + const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url); return { isLocal, - stream: isLocal ? fs.createReadStream : request + stream: isLocal ? fs.createReadStream : request, + normalized: isLocal ? path.normalize(url) : url }; }; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 66668aaef..fabc18cfd 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx9BwKT8sxbXNR78f2Ks3pAe2DfsxOhrYMj8SACNi13xwJ0MtLU4WYb4_cbHAj2X8imZW9eUBlAsY9RXoMEPOmVpMlhMjxVZKBo_0lwJ6xSTunSdrR1e8P7vkRV4Q","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567962258021} \ No newline at end of file +{"access_token":"ya29.Glx-BwgWcpQUukTNyuUqvSAYrDyxDNUhCLtrFDJAViROvicm0DrcRvCn4OaQdn2m2IZQYcG-19cvQYoOC3UJCtWXLRvKZzQCbZZSykpxYu_lflUyEnIGZOIHMbbEjA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568008211814} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 54525cd31..baef94a59 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -561,6 +561,11 @@ class NodeCanvasFactory { const pngTypes = [".png", ".PNG"]; const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"]; const uploadDirectory = __dirname + "/public/files/"; +interface FileResponse { + name: string; + path: string; + type: string; +} // SETTERS app.post( RouteStore.upload, @@ -569,13 +574,16 @@ app.post( form.uploadDir = uploadDirectory; form.keepExtensions = true; form.parse(req, async (_err, _fields, files) => { - let names: string[] = []; - for (const name in files) { - const file = path.basename(files[name].path); - await UploadUtils.UploadImage(uploadDirectory + file, file); - names.push(`/files/` + file); + let results: FileResponse[] = []; + for (const key in files) { + const { name, type, path: location } = files[key]; + const filename = path.basename(location); + await UploadUtils.UploadImage(uploadDirectory + filename, path.basename(name)); + results.push({ name, type, path: `/files/${filename}` }); + console.log(path.basename(name)); } - res.send(names); + console.log("All files traversed!"); + _success(res, results); }); } ); @@ -843,7 +851,7 @@ app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { UploadUtils.UploadImage(item.baseUrl, item.filename, prefix) ); const completed = await Promise.all(pending).catch(error => _error(res, downloadError, error)); - _success(res, completed); + Array.isArray(completed) && _success(res, completed); return; } _invalid(res, requestError); -- cgit v1.2.3-70-g09d2 From b24c475d8cd36af860fc374b0c5621b0d096be1d Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 9 Sep 2019 20:47:34 -0400 Subject: nearly finished transferring images between text notes and google docs --- package.json | 2 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 23 ++-- .../util/Import & Export/DirectoryImportBox.tsx | 2 +- src/client/views/MainView.tsx | 6 +- src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/new_fields/RichTextUtils.ts | 67 ++++++++++- src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 2 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 5 + src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 20 +++- src/server/updateSearch.ts | 123 --------------------- 13 files changed, 112 insertions(+), 147 deletions(-) delete mode 100644 src/server/updateSearch.ts (limited to 'src/server') diff --git a/package.json b/package.json index f56e34ce0..f0f2b467e 100644 --- a/package.json +++ b/package.json @@ -224,4 +224,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} \ No newline at end of file +} diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 5f5b39b14..fddcf3aa5 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -51,17 +51,26 @@ export namespace GooglePhotosClientUtils { description: string; } - export const UploadImageDocuments = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { + export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => { if (album && "title" in album) { album = await (await endpoint()).albums.create(album.title); } const media: MediaInput[] = []; - sources.forEach(document => { - const data = Cast(Doc.GetProto(document).data, ImageField); - data && media.push({ - url: data.url.href, - description: parseDescription(document, descriptionKey), - }); + sources.forEach(source => { + let url: string; + let description: string; + if (source instanceof Doc) { + const data = Cast(Doc.GetProto(source).data, ImageField); + if (!data) { + return; + } + url = data.url.href; + description = parseDescription(source, descriptionKey); + } else { + url = source; + description = Utils.GenerateGuid(); + } + media.push({ url, description }); }); if (media.length) { return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 7693a388f..a19fd39b7 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -117,7 +117,7 @@ export default class DirectoryImportBox extends React.Component console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`); })); - await GooglePhotosClientUtils.UploadImageDocuments(docs, { title: directory }); + await GooglePhotosClientUtils.UploadImages(docs, { title: directory }); console.log("Finished upload!"); for (let i = 0; i < docs.length; i++) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 326c13424..0c0ed9072 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -155,7 +155,7 @@ export class MainView extends React.Component { doc.caption = "Well isn't this a nice cat image!"; let photos = await GooglePhotosClientUtils.endpoint(); let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; - console.log(await GooglePhotosClientUtils.UploadImageDocuments([doc], { id: albumId })); + console.log(await GooglePhotosClientUtils.UploadImages([doc], { id: albumId })); } componentWillUnMount() { @@ -470,7 +470,7 @@ export class MainView extends React.Component { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); - let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); + // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); let btns: [React.RefObject, IconName, string, () => Doc | Promise][] = [ [React.createRef(), "object-group", "Add Collection", addColNode], @@ -478,7 +478,7 @@ export class MainView extends React.Component { [React.createRef(), "globe-asia", "Add Website", addWebNode], [React.createRef(), "bolt", "Add Button", addButtonDocument], [React.createRef(), "file", "Add Document Dragger", addDragboxNode], - [React.createRef(), "object-group", "Test Google Photos Search", googlePhotosSearch], + // [React.createRef(), "object-group", "Test Google Photos Search", googlePhotosSearch], [React.createRef(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode //[React.createRef(), "play", "Add Youtube Searcher", addYoutubeSearcher], ]; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 99e5ab7b3..5fc4f36a7 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -253,7 +253,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; - let path = Utils.prepend(file); + let path = Utils.prepend(file.path); Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); })); }); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 4033ffd9c..cb9346a8b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -591,7 +591,7 @@ export class DocumentView extends DocComponent(Docu subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); if (Cast(this.props.Document.data, ImageField)) { - cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImageDocuments([this.props.Document]), icon: "caret-square-right" }); + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImages([this.props.Document]), icon: "caret-square-right" }); } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 0aba50c0d..500b93676 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -7,6 +7,11 @@ import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox"; import { Opt } from "./Doc"; import * as Color from "color"; import { sinkListItem } from "prosemirror-schema-list"; +import { Utils, PostToServer } from "../Utils"; +import { RouteStore } from "../server/RouteStore"; +import { Docs } from "../client/documents/Documents"; +import { schema } from "../client/util/RichTextSchema"; +import { GooglePhotosClientUtils } from "../client/apis/google_docs/GooglePhotosClientUtils"; export namespace RichTextUtils { @@ -86,19 +91,36 @@ export namespace RichTextUtils { export namespace GoogleDocs { export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => { - let textNodes: Node[] = []; + let nodes: { [type: string]: Node[] } = { + text: [], + image: [] + }; 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); + content.forEach(node => node.content.forEach(node => { + const type = node.type.name; + let existing = nodes[type]; + if (existing) { + existing.push(node); + } else { + nodes[type] = [node]; + } + })); + let linkRequests = ExtractLinks(nodes.text); + let imageRequests = ExtractImages(nodes.image); return { text, - requests: [...linkRequests] + requests: [...linkRequests, ...imageRequests] }; }; type BulletPosition = { value: number, sinks: number }; + interface MediaItem { + baseUrl: string; + filename: string; + width: number; + } export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise> => { const document = await GoogleApiClientUtils.Docs.retrieve({ documentId }); if (!document) { @@ -109,6 +131,17 @@ export namespace RichTextUtils { const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document); let state = FormattedTextBox.blankState(); let structured = parseLists(paragraphs); + const inline = document.inlineObjects; + let inlineUrls: MediaItem[] = []; + if (inline) { + inlineUrls = Object.keys(inline).map(key => { + const embedded = inline[key].inlineObjectProperties!.embeddedObject!; + const baseUrl = embedded.imageProperties!.contentUri!; + const filename = `upload_${Utils.GenerateGuid()}.png`; + const width = embedded.size!.width!.magnitude!; + return { baseUrl, filename, width }; + }); + } let position = 3; let lists: ListGroup[] = []; @@ -151,6 +184,12 @@ export namespace RichTextUtils { } } + const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems: inlineUrls }); + for (let i = 0; i < uploads.length; i++) { + const src = Utils.prepend(`/files/${uploads[i].fileNames.clean}`); + state = state.apply(state.tr.insert(0, schema.nodes.image.create({ src, width: inlineUrls[i].width }))); + } + return { title, text, state }; }; @@ -252,6 +291,26 @@ export namespace RichTextUtils { return links; }; + const ExtractImages = async (nodes: Node[]) => { + const images: docs_v1.Schema$Request[] = []; + let position = 1; + for (let node of nodes) { + const length = node.nodeSize; + const attrs = node.attrs; + const uri = attrs.src; + const result = (await GooglePhotosClientUtils.UploadImages([uri])).newMediaItemResults; + images.push({ + insertInlineImage: { + uri: result[0].mediaItem.productUrl, + objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } }, + location: { index: position + length } + } + }); + position += length; + } + return images; + }; + const Encode = (information: LinkInformation) => { return { updateTextStyle: { diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index f65e6134c..ee9cd8a0e 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -34,6 +34,7 @@ export enum RouteStore { googleDocs = "/googleDocs", googlePhotosAccessToken = "/googlePhotosAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", - googlePhotosMediaDownload = "/googlePhotosMediaDownload" + googlePhotosMediaDownload = "/googlePhotosMediaDownload", + googleDocsGet = "/googleDocsGet" } \ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index ac8023ce1..e0bd8a800 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -42,7 +42,7 @@ export namespace GoogleApiServerUtils { export type ApiResponse = Promise; export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse; - export type ApiHandler = (parameters: any) => ApiResponse; + export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse; export type Action = "create" | "retrieve" | "update"; export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler }; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 35f986250..d1f1f81bd 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -151,6 +151,11 @@ export namespace DownloadUtils { .on('close', resolve) .on('error', reject); }); + if (!isLocal) { + await new Promise(resolve => { + stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve); + }); + } } resolve(information); }); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index fabc18cfd..5c142fba1 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx-BwgWcpQUukTNyuUqvSAYrDyxDNUhCLtrFDJAViROvicm0DrcRvCn4OaQdn2m2IZQYcG-19cvQYoOC3UJCtWXLRvKZzQCbZZSykpxYu_lflUyEnIGZOIHMbbEjA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568008211814} \ No newline at end of file +{"access_token":"ya29.Glx_B6G7Q_FYs1LK5VcyV6P6Zg9JkoHO2aC_TsnN7AVxPYWHEpsBSC0WyWX7Ztr8HWhOUYA5JXqnZDkLrK1V3Hb-0GgtyApLRNtEPOWf1dJ7lOm_iKVw2tRvPe7XDQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568078116605} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index baef94a59..8469770d5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -38,6 +38,7 @@ import flash = require('connect-flash'); import { Search } from './Search'; import _ = require('lodash'); import * as Archiver from 'archiver'; +import * as request_promise from 'request-promise'; var AdmZip = require('adm-zip'); import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; @@ -576,9 +577,9 @@ app.post( form.parse(req, async (_err, _fields, files) => { let results: FileResponse[] = []; for (const key in files) { - const { name, type, path: location } = files[key]; + const { type, path: location, name } = files[key]; const filename = path.basename(location); - await UploadUtils.UploadImage(uploadDirectory + filename, path.basename(name)); + await UploadUtils.UploadImage(uploadDirectory + filename, filename); results.push({ name, type, path: `/files/${filename}` }); console.log(path.basename(name)); } @@ -790,10 +791,23 @@ const tokenPath = path.join(__dirname, "./credentials/google_docs_token.json"); const EndpointHandlerMap = new Map([ ["create", (api, params) => api.create(params)], - ["retrieve", (api, params) => api.get(params)], + ["retrieve", (api, params) => api.get(params, { params: "fields=inlineObjects" })], ["update", (api, params) => api.batchUpdate(params)], ]); +// app.post(RouteStore.googleDocsGet, async (req, res) => { +// const token = await GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath }); +// request_promise.get({ +// uri: `https://docs.googleapis.com/v1/documents/${req.body.documentId}?fields=inlineObjects`, +// headers: { +// 'Authorization': `Bearer ${token}` +// } +// }).then(response => { +// console.log(response); +// res.send(response); +// }); +// }); + app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { let sector: GoogleApiServerUtils.Service = req.params.sector; let action: GoogleApiServerUtils.Action = req.params.action; diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts deleted file mode 100644 index 906b795f1..000000000 --- a/src/server/updateSearch.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Database } from "./database"; -import { Cursor } from "mongodb"; -import { Search } from "./Search"; -import pLimit from 'p-limit'; - -const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { - "number": "_n", - "string": "_t", - "boolean": "_b", - // "image": ["_t", "url"], - "video": ["_t", "url"], - "pdf": ["_t", "url"], - "audio": ["_t", "url"], - "web": ["_t", "url"], - "date": ["_d", value => new Date(value.date).toISOString()], - "proxy": ["_i", "fieldId"], - "list": ["_l", list => { - const results = []; - for (const value of list.fields) { - const term = ToSearchTerm(value); - if (term) { - results.push(term.value); - } - } - return results.length ? results : null; - }] -}; - -function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { - if (val === null || val === undefined) { - return; - } - const type = val.__type || typeof val; - let suffix = suffixMap[type]; - if (!suffix) { - return; - } - - if (Array.isArray(suffix)) { - const accessor = suffix[1]; - if (typeof accessor === "function") { - val = accessor(val); - } else { - val = val[accessor]; - } - suffix = suffix[0]; - } - - return { suffix, value: val }; -} - -function getSuffix(value: string | [string, any]): string { - return typeof value === "string" ? value : value[0]; -} - -const limit = pLimit(5); -async function update() { - // await new Promise(res => setTimeout(res, 5)); - console.log("update"); - await Search.Instance.clear(); - const cursor = await Database.Instance.query({}); - console.log("Cleared"); - const updates: any[] = []; - let numDocs = 0; - function updateDoc(doc: any) { - numDocs++; - if ((numDocs % 50) === 0) { - console.log("updateDoc " + numDocs); - } - // console.log("doc " + numDocs); - if (doc.__type !== "Doc") { - return; - } - const fields = doc.fields; - if (!fields) { - return; - } - const update: any = { id: doc._id }; - let dynfield = false; - for (const key in fields) { - const value = fields[key]; - const term = ToSearchTerm(value); - if (term !== undefined) { - let { suffix, value } = term; - update[key + suffix] = value; - dynfield = true; - } - } - if (dynfield) { - updates.push(update); - // console.log(updates.length); - } - } - await cursor.forEach(updateDoc); - console.log(`Updating ${updates.length} documents`); - const result = await Search.Instance.updateDocuments(updates); - try { - console.log(JSON.parse(result).responseHeader.status); - } catch { - console.log("Error:"); - // console.log(updates[i]); - console.log(result); - console.log("\n"); - } - // for (let i = 0; i < updates.length; i++) { - // console.log(i); - // const result = await Search.Instance.updateDocument(updates[i]); - // try { - // console.log(JSON.parse(result).responseHeader.status); - // } catch { - // console.log("Error:"); - // console.log(updates[i]); - // console.log(result); - // console.log("\n"); - // } - // } - // await Promise.all(updates.map(update => { - // return limit(() => Search.Instance.updateDocument(update)); - // })); - cursor.close(); -} - -update(); \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 628eef55533118b1f2312b86b2ac5f7b64f7fc4a Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 10 Sep 2019 20:01:55 -0400 Subject: lots of refactoring, beginning autotagging --- src/Utils.ts | 5 + .../apis/google_docs/GooglePhotosClientUtils.ts | 339 ++++++++++++++------- .../util/Import & Export/DirectoryImportBox.tsx | 5 +- src/client/views/MainView.tsx | 18 +- src/client/views/nodes/DocumentView.tsx | 7 +- src/client/views/nodes/FormattedTextBox.tsx | 6 +- src/new_fields/RichTextUtils.ts | 13 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 7 +- src/server/apis/google/SharedTypes.ts | 21 ++ src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 21 +- 11 files changed, 284 insertions(+), 160 deletions(-) create mode 100644 src/server/apis/google/SharedTypes.ts (limited to 'src/server') diff --git a/src/Utils.ts b/src/Utils.ts index 959b89fe5..71d88683a 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -37,6 +37,11 @@ export class Utils { public static prepend(extension: string): string { return window.location.origin + extension; } + + public static fileUrl(filename: string): string { + return this.prepend(`/file/${filename}`); + } + public static CorsProxy(url: string): string { return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url); } diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index fddcf3aa5..b1de24d1a 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,8 +1,8 @@ import { PostToServer, Utils } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { ImageField } from "../../../new_fields/URLField"; -import { Cast } from "../../../new_fields/Types"; -import { Doc, Opt } from "../../../new_fields/Doc"; +import { Cast, StrCast } from "../../../new_fields/Types"; +import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import Photos = require('googlephotos'); import { RichTextField } from "../../../new_fields/RichTextField"; @@ -10,32 +10,15 @@ import { RichTextUtils } from "../../../new_fields/RichTextUtils"; import { EditorState } from "prosemirror-state"; import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { type } from "os"; - -export namespace GooglePhotosClientUtils { - - export enum ContentCategories { - NONE = 'NONE', - LANDSCAPES = 'LANDSCAPES', - RECEIPTS = 'RECEIPTS', - CITYSCAPES = 'CITYSCAPES', - LANDMARKS = 'LANDMARKS', - SELFIES = 'SELFIES', - PEOPLE = 'PEOPLE', - PETS = 'PETS', - WEDDINGS = 'WEDDINGS', - BIRTHDAYS = 'BIRTHDAYS', - DOCUMENTS = 'DOCUMENTS', - TRAVEL = 'TRAVEL', - ANIMALS = 'ANIMALS', - FOOD = 'FOOD', - SPORT = 'SPORT', - NIGHT = 'NIGHT', - PERFORMANCES = 'PERFORMANCES', - WHITEBOARDS = 'WHITEBOARDS', - SCREENSHOTS = 'SCREENSHOTS', - UTILITY = 'UTILITY' - } +import { MediaItemCreationResult, NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; + +export namespace GooglePhotos { + + const endpoint = async () => { + const getToken = Utils.prepend(RouteStore.googlePhotosAccessToken); + const token = await (await fetch(getToken)).text(); + return new Photos(token); + }; export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', @@ -44,118 +27,238 @@ export namespace GooglePhotosClientUtils { } export type AlbumReference = { id: string } | { title: string }; - export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text())); export interface MediaInput { url: string; description: string; } - export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => { - if (album && "title" in album) { - album = await (await endpoint()).albums.create(album.title); + export const ContentCategories = { + NONE: 'NONE', + LANDSCAPES: 'LANDSCAPES', + RECEIPTS: 'RECEIPTS', + CITYSCAPES: 'CITYSCAPES', + LANDMARKS: 'LANDMARKS', + SELFIES: 'SELFIES', + PEOPLE: 'PEOPLE', + PETS: 'PETS', + WEDDINGS: 'WEDDINGS', + BIRTHDAYS: 'BIRTHDAYS', + DOCUMENTS: 'DOCUMENTS', + TRAVEL: 'TRAVEL', + ANIMALS: 'ANIMALS', + FOOD: 'FOOD', + SPORT: 'SPORT', + NIGHT: 'NIGHT', + PERFORMANCES: 'PERFORMANCES', + WHITEBOARDS: 'WHITEBOARDS', + SCREENSHOTS: 'SCREENSHOTS', + UTILITY: 'UTILITY' + }; + + export namespace Export { + + export interface AlbumCreationResult { + albumId: string; + mediaItems: MediaItem[]; } - const media: MediaInput[] = []; - sources.forEach(source => { - let url: string; - let description: string; - if (source instanceof Doc) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; + + export const CollectionToAlbum = async (collection: Doc, title?: string, descriptionKey?: string): Promise> => { + const dataDocument = Doc.GetProto(collection); + const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); + if (!images || !images.length) { + return undefined; + } + const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); + const { id } = await Create.Album(resolved); + const result = await Transactions.UploadImages(images, { id }, descriptionKey); + if (result) { + const mediaItems = result.newMediaItemResults.map(item => item.mediaItem); + return { albumId: id, mediaItems }; + } + }; + + } + + export namespace Import { + + export type CollectionConstructor = (data: Array, options: DocumentOptions, ...args: any) => Doc; + + export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt>): Promise => { + let response = await Query.Search(requested); + let uploads = await Transactions.WriteMediaItemsToServer(response); + const children = uploads.map((upload: Transactions.UploadInformation) => { + let document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean)); + document.fillColumn = true; + document.contentSize = upload.contentSize; + return document; + }); + const options = { width: 500, height: 500 }; + return constructor(children, options); + }; + + } + + export namespace Query { + + export const AppendImageMetadata = (sources: (Doc | string)[]) => { + let keys = Object.keys(ContentCategories); + let included: string[] = []; + let excluded: string[] = []; + for (let i = 0; i < keys.length; i++) { + for (let j = 0; j < keys.length; j++) { + let value = ContentCategories[keys[i] as keyof typeof ContentCategories]; + if (j === i) { + included.push(value); + } else { + excluded.push(value); + } } - url = data.url.href; - description = parseDescription(source, descriptionKey); - } else { - url = source; - description = Utils.GenerateGuid(); + //... + included = excluded = []; } - media.push({ url, description }); - }); - if (media.length) { - return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + }; + + interface DateRange { + after: Date; + before: Date; } - }; - const parseDescription = (document: Doc, descriptionKey: string) => { - let description: string = Utils.prepend("/doc/" + document[Id]); - const target = document[descriptionKey]; - if (typeof target === "string") { - description = target; - } else if (target instanceof RichTextField) { - description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data))); + const DefaultSearchOptions: SearchOptions = { + pageSize: 20, + included: [], + excluded: [], + date: undefined, + includeArchivedMedia: true, + type: MediaType.ALL_MEDIA, + }; + + export interface SearchOptions { + pageSize: number; + included: ContentCategories[]; + excluded: ContentCategories[]; + date: Opt; + includeArchivedMedia: boolean; + type: MediaType; } - return description; - }; - export interface DateRange { - after: Date; - before: Date; - } - export interface SearchOptions { - pageSize: number; - included: ContentCategories[]; - excluded: ContentCategories[]; - date: Opt; - includeArchivedMedia: boolean; - type: MediaType; + export interface SearchResponse { + mediaItems: any[]; + nextPageToken: string; + } + + export const Search = async (requested: Opt>): Promise => { + const options = requested || DefaultSearchOptions; + const photos = await endpoint(); + const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); + + const included = options.included || []; + const excluded = options.excluded || []; + const contentFilter = new photos.ContentFilter(); + included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category)); + excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); + filters.setContentFilter(contentFilter); + + const date = options.date; + if (date) { + const dateFilter = new photos.DateFilter(); + if (date instanceof Date) { + dateFilter.addDate(date); + } else { + dateFilter.addRange(date.after, date.before); + } + filters.setDateFilter(dateFilter); + } + + filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); + + return new Promise(resolve => { + photos.mediaItems.search(filters, options.pageSize || 20).then(resolve); + }); + }; + + export const GetImage = async (mediaItemId: string): Promise => { + return (await endpoint()).mediaItems.get(mediaItemId); + }; + } - const DefaultSearchOptions: SearchOptions = { - pageSize: 20, - included: [], - excluded: [], - date: undefined, - includeArchivedMedia: true, - type: MediaType.ALL_MEDIA, - }; + export namespace Create { + + export const Album = async (title: string) => { + return (await endpoint()).albums.create(title); + }; - export interface SearchResponse { - mediaItems: any[]; - nextPageToken: string; } - export type CollectionConstructor = (data: Array, options: DocumentOptions, ...args: any) => Doc; - export const CollectionFromSearch = async (provider: CollectionConstructor, requested: Opt>): Promise => { - let downloads = await Search(requested); - return provider(downloads.map((download: any) => { - let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileNames.clean}`)); - document.fillColumn = true; - document.contentSize = download.contentSize; - return document; - }), { width: 500, height: 500 }); - }; + export namespace Transactions { - export const Search = async (requested: Opt>): Promise => { - const options = requested || DefaultSearchOptions; - const photos = await endpoint(); - const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); - - const included = options.included || []; - const excluded = options.excluded || []; - const contentFilter = new photos.ContentFilter(); - included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category)); - excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); - filters.setContentFilter(contentFilter); - - const date = options.date; - if (date) { - const dateFilter = new photos.DateFilter(); - if (date instanceof Date) { - dateFilter.addDate(date); - } else { - dateFilter.addRange(date.after, date.before); - } - filters.setDateFilter(dateFilter); + export interface UploadInformation { + mediaPaths: string[]; + fileNames: { [key: string]: string }; + contentSize?: number; + contentType?: string; + } + + export interface MediaItem { + id: string; + filename: string; + baseUrl: string; } - filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); + export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise => { + const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, body); + return uploads; + }; - return new Promise(resolve => { - photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => { - response && resolve(await PostToServer(RouteStore.googlePhotosMediaDownload, response)); + export const UploadThenFetch = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => { + const result = await UploadImages(sources, album, descriptionKey); + if (!result) { + return undefined; + } + const baseUrls: string[] = await Promise.all(result.newMediaItemResults.map((result: any) => { + return new Promise(resolve => Query.GetImage(result.mediaItem.id).then(item => resolve(item.baseUrl))); + })); + return baseUrls; + }; + + export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + if (album && "title" in album) { + album = await Create.Album(album.title); + } + const media: MediaInput[] = []; + sources.forEach(source => { + let url: string; + let description: string; + if (source instanceof Doc) { + const data = Cast(Doc.GetProto(source).data, ImageField); + if (!data) { + return; + } + url = data.url.href; + description = parseDescription(source, descriptionKey); + } else { + url = source; + description = Utils.GenerateGuid(); + } + media.push({ url, description }); }); - }); - }; + if (media.length) { + return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + } + }; + + const parseDescription = (document: Doc, descriptionKey: string) => { + let description: string = Utils.prepend("/doc/" + document[Id]); + const target = document[descriptionKey]; + if (typeof target === "string") { + description = target; + } else if (target instanceof RichTextField) { + description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data))); + } + return description; + }; + + } } \ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index a19fd39b7..d58c02ce5 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -18,7 +18,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; -import { GooglePhotosClientUtils } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; const unsupported = ["text/html", "text/plain"]; interface FileResponse { @@ -117,8 +117,7 @@ export default class DirectoryImportBox extends React.Component console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`); })); - await GooglePhotosClientUtils.UploadImages(docs, { title: directory }); - console.log("Finished upload!"); + await GooglePhotos.Transactions.UploadImages(docs, { title: directory }); for (let i = 0; i < docs.length; i++) { let doc = docs[i]; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 0c0ed9072..8d10a91ce 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -40,7 +40,7 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; -import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../new_fields/URLField'; import { LinkFollowBox } from './linking/LinkFollowBox'; import { DocumentManager } from '../util/DocumentManager'; @@ -149,14 +149,14 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } - executeGooglePhotosRoutine = async () => { - let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); - doc.caption = "Well isn't this a nice cat image!"; - let photos = await GooglePhotosClientUtils.endpoint(); - let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; - console.log(await GooglePhotosClientUtils.UploadImages([doc], { id: albumId })); - } + // executeGooglePhotosRoutine = async () => { + // let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + // let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + // doc.caption = "Well isn't this a nice cat image!"; + // let photos = await GooglePhotos.endpoint(); + // let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; + // console.log(await GooglePhotos.UploadImages([doc], { id: albumId })); + // } componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index cb9346a8b..a51f783ad 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,7 +41,7 @@ import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; -import { GooglePhotosClientUtils } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../../new_fields/URLField'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -591,7 +591,10 @@ export class DocumentView extends DocComponent(Docu subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); if (Cast(this.props.Document.data, ImageField)) { - cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImages([this.props.Document]), icon: "caret-square-right" }); + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum(this.props.Document).then(console.log), icon: "caret-square-right" }); } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fda9ea33f..9d83fbd04 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -177,7 +177,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe syncNodeSelection(view: any, sel: any) { if (sel instanceof NodeSelection) { var desc = view.docView.descAt(sel.from); - if (desc != view.lastSelectedViewDesc) { + if (desc !== view.lastSelectedViewDesc) { if (view.lastSelectedViewDesc) { view.lastSelectedViewDesc.deselectNode(); view.lastSelectedViewDesc = null; @@ -463,7 +463,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } let redo = async () => { if (this._editorView && reference) { - let content = RichTextUtils.GoogleDocs.Export(this._editorView.state); + let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); @@ -636,7 +636,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }, - footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) } + footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 500b93676..27737782b 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -11,7 +11,7 @@ import { Utils, PostToServer } from "../Utils"; import { RouteStore } from "../server/RouteStore"; import { Docs } from "../client/documents/Documents"; import { schema } from "../client/util/RichTextSchema"; -import { GooglePhotosClientUtils } from "../client/apis/google_docs/GooglePhotosClientUtils"; +import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils"; export namespace RichTextUtils { @@ -90,7 +90,7 @@ export namespace RichTextUtils { export namespace GoogleDocs { - export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => { + export const Export = async (state: EditorState): Promise => { let nodes: { [type: string]: Node[] } = { text: [], image: [] @@ -107,7 +107,7 @@ export namespace RichTextUtils { } })); let linkRequests = ExtractLinks(nodes.text); - let imageRequests = ExtractImages(nodes.image); + let imageRequests = await ExtractImages(nodes.image); return { text, requests: [...linkRequests, ...imageRequests] @@ -298,10 +298,13 @@ export namespace RichTextUtils { const length = node.nodeSize; const attrs = node.attrs; const uri = attrs.src; - const result = (await GooglePhotosClientUtils.UploadImages([uri])).newMediaItemResults; + const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([uri]); + if (!baseUrls) { + continue; + } images.push({ insertInlineImage: { - uri: result[0].mediaItem.productUrl, + uri: baseUrls[0], objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } }, location: { index: position + length } } diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index d1f1f81bd..447ed23ac 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -5,6 +5,7 @@ import { Utils } from '../../../Utils'; import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; import * as sharp from 'sharp'; +import { MediaItemCreationResult } from './SharedTypes'; const uploadDirectory = path.join(__dirname, "../../public/files/"); @@ -52,8 +53,10 @@ export namespace GooglePhotosUploadUtils { return new Promise(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body))); }; - export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => { - return new Promise((resolve, reject) => { + + + export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }): Promise => { + return new Promise((resolve, reject) => { const parameters = { method: 'POST', headers: headers('json'), diff --git a/src/server/apis/google/SharedTypes.ts b/src/server/apis/google/SharedTypes.ts new file mode 100644 index 000000000..9ad6130b6 --- /dev/null +++ b/src/server/apis/google/SharedTypes.ts @@ -0,0 +1,21 @@ +export interface NewMediaItemResult { + uploadToken: string; + status: { code: number, message: string }; + mediaItem: MediaItem; +} + +export interface MediaItem { + id: string; + description: string; + productUrl: string; + baseUrl: string; + mimeType: string; + mediaMetadata: { + creationTime: string; + width: string; + height: string; + }; + filename: string; +} + +export type MediaItemCreationResult = { newMediaItemResults: NewMediaItemResult[] }; \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 5c142fba1..22d57d744 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx_B6G7Q_FYs1LK5VcyV6P6Zg9JkoHO2aC_TsnN7AVxPYWHEpsBSC0WyWX7Ztr8HWhOUYA5JXqnZDkLrK1V3Hb-0GgtyApLRNtEPOWf1dJ7lOm_iKVw2tRvPe7XDQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568078116605} \ No newline at end of file +{"access_token":"ya29.GlyAB5T3dgJqWuYBcLaT94wQo7MZkmzJQZxDB2sSU95mdhW24E3diuFdLeNsUDVI57D3S765RweMnL98d-fdgu1dRxpzkV_J_3rLih99pZ8A4d6jVdm1354UT4py_w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568161931458} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 8469770d5..2c3e76c55 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -795,19 +795,6 @@ const EndpointHandlerMap = new Map api.batchUpdate(params)], ]); -// app.post(RouteStore.googleDocsGet, async (req, res) => { -// const token = await GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath }); -// request_promise.get({ -// uri: `https://docs.googleapis.com/v1/documents/${req.body.documentId}?fields=inlineObjects`, -// headers: { -// 'Authorization': `Bearer ${token}` -// } -// }).then(response => { -// console.log(response); -// res.send(response); -// }); -// }); - app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { let sector: GoogleApiServerUtils.Service = req.params.sector; let action: GoogleApiServerUtils.Action = req.params.action; @@ -841,11 +828,11 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { }; })); if (!newMediaItems.every(item => item)) { - return res.status(STATUS.EXECUTION_ERROR).send(tokenError); + return _error(res, tokenError); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - success => res.status(STATUS.OK).send(success), - () => res.status(STATUS.EXECUTION_ERROR).send(mediaError) + mediaItems => _success(res, mediaItems), + error => _error(res, mediaError, error) ); }); @@ -871,7 +858,7 @@ app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { _invalid(res, requestError); }); -const _error = (res: Response, message: string, error: any) => { +const _error = (res: Response, message: string, error?: any) => { res.statusMessage = message; res.status(STATUS.EXECUTION_ERROR).send(error); }; -- cgit v1.2.3-70-g09d2 From 5a0794a74ce62612435133907395482f494747f4 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 11 Sep 2019 12:19:30 -0400 Subject: now support auto tagging --- .../apis/google_docs/GooglePhotosClientUtils.ts | 126 ++++++++++++++------- .../util/Import & Export/DirectoryImportBox.tsx | 7 +- src/client/views/MainView.tsx | 19 ++-- src/client/views/nodes/DocumentView.tsx | 3 +- src/new_fields/RichTextUtils.ts | 2 +- .../apis/google/CustomizedWrapper/filters.js | 46 ++++++++ src/server/apis/google/GooglePhotosUploadUtils.ts | 34 ++++-- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 2 +- 9 files changed, 168 insertions(+), 73 deletions(-) create mode 100644 src/server/apis/google/CustomizedWrapper/filters.js (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index b1de24d1a..a28b183d1 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -10,7 +10,10 @@ import { RichTextUtils } from "../../../new_fields/RichTextUtils"; import { EditorState } from "prosemirror-state"; import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { MediaItemCreationResult, NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; +import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; +import { AssertionError } from "assert"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; export namespace GooglePhotos { @@ -53,7 +56,14 @@ export namespace GooglePhotos { PERFORMANCES: 'PERFORMANCES', WHITEBOARDS: 'WHITEBOARDS', SCREENSHOTS: 'SCREENSHOTS', - UTILITY: 'UTILITY' + UTILITY: 'UTILITY', + ARTS: 'ARTS', + CRAFTS: 'CRAFTS', + FASHION: 'FASHION', + HOUSES: 'HOUSES', + GARDENS: 'GARDENS', + FLOWERS: 'FLOWERS', + HOLIDAYS: 'HOLIDAYS' }; export namespace Export { @@ -63,7 +73,15 @@ export namespace GooglePhotos { mediaItems: MediaItem[]; } - export const CollectionToAlbum = async (collection: Doc, title?: string, descriptionKey?: string): Promise> => { + export interface AlbumCreationOptions { + collection: Doc; + title?: string; + descriptionKey?: string; + tag?: boolean; + } + + export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise> => { + const { collection, title, descriptionKey, tag } = options; const dataDocument = Doc.GetProto(collection); const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); if (!images || !images.length) { @@ -71,9 +89,24 @@ export namespace GooglePhotos { } const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); const { id } = await Create.Album(resolved); - const result = await Transactions.UploadImages(images, { id }, descriptionKey); - if (result) { - const mediaItems = result.newMediaItemResults.map(item => item.mediaItem); + const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey); + if (newMediaItemResults) { + const mediaItems = newMediaItemResults.map(item => item.mediaItem); + if (mediaItems.length !== images.length) { + throw new AssertionError({ actual: mediaItems.length, expected: images.length }); + } + const idMapping = new Doc; + for (let i = 0; i < images.length; i++) { + const image = images[i]; + const mediaItem = mediaItems[i]; + image.googlePhotosId = mediaItem.id; + image.googlePhotosUrl = mediaItem.baseUrl || mediaItem.productUrl; + idMapping[mediaItem.id] = image; + } + collection.googlePhotosIdMapping = idMapping; + if (tag) { + await Query.AppendImageMetadata(collection); + } return { albumId: id, mediaItems }; } }; @@ -101,21 +134,32 @@ export namespace GooglePhotos { export namespace Query { - export const AppendImageMetadata = (sources: (Doc | string)[]) => { - let keys = Object.keys(ContentCategories); - let included: string[] = []; - let excluded: string[] = []; - for (let i = 0; i < keys.length; i++) { - for (let j = 0; j < keys.length; j++) { - let value = ContentCategories[keys[i] as keyof typeof ContentCategories]; - if (j === i) { - included.push(value); - } else { - excluded.push(value); + export const AppendImageMetadata = async (collection: Doc) => { + const idMapping = await Cast(collection.googlePhotosIdMapping, Doc); + if (!idMapping) { + throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!"); + } + const images = await DocListCastAsync(collection.data); + images && images.forEach(image => image.googlePhotosTags = new List()); + const values = Object.values(ContentCategories); + for (let value of values) { + console.log("Searching for ", value); + const results = await Search({ included: [value] }); + if (results.mediaItems) { + console.log(`${results.mediaItems.length} found!`); + const ids = results.mediaItems.map(item => item.id); + for (let id of ids) { + const image = await Cast(idMapping[id], Doc); + if (image) { + const tags = Cast(image.googlePhotosTags, listSpec("string"))!; + if (!tags.includes(value)) { + tags.push(value); + console.log(`${value}: ${id}`); + } + } } } - //... - included = excluded = []; + console.log(); } }; @@ -125,20 +169,22 @@ export namespace GooglePhotos { } const DefaultSearchOptions: SearchOptions = { - pageSize: 20, + pageSize: 50, included: [], excluded: [], date: undefined, includeArchivedMedia: true, + excludeNonAppCreatedData: false, type: MediaType.ALL_MEDIA, }; export interface SearchOptions { pageSize: number; - included: ContentCategories[]; - excluded: ContentCategories[]; + included: string[]; + excluded: string[]; date: Opt; includeArchivedMedia: boolean; + excludeNonAppCreatedData: boolean; type: MediaType; } @@ -173,7 +219,7 @@ export namespace GooglePhotos { filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); return new Promise(resolve => { - photos.mediaItems.search(filters, options.pageSize || 20).then(resolve); + photos.mediaItems.search(filters, options.pageSize || 100).then(resolve); }); }; @@ -183,7 +229,7 @@ export namespace GooglePhotos { } - export namespace Create { + namespace Create { export const Album = async (title: string) => { return (await endpoint()).albums.create(title); @@ -211,40 +257,34 @@ export namespace GooglePhotos { return uploads; }; - export const UploadThenFetch = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => { - const result = await UploadImages(sources, album, descriptionKey); - if (!result) { + export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { + const newMediaItems = await UploadImages(sources, album, descriptionKey); + if (!newMediaItems) { return undefined; } - const baseUrls: string[] = await Promise.all(result.newMediaItemResults.map((result: any) => { - return new Promise(resolve => Query.GetImage(result.mediaItem.id).then(item => resolve(item.baseUrl))); + const baseUrls: string[] = await Promise.all(newMediaItems.map(item => { + return new Promise(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); })); return baseUrls; }; - export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { if (album && "title" in album) { album = await Create.Album(album.title); } const media: MediaInput[] = []; sources.forEach(source => { - let url: string; - let description: string; - if (source instanceof Doc) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; - } - url = data.url.href; - description = parseDescription(source, descriptionKey); - } else { - url = source; - description = Utils.GenerateGuid(); + const data = Cast(Doc.GetProto(source).data, ImageField); + if (!data) { + return; } + const url = data.url.href; + const description = parseDescription(source, descriptionKey); media.push({ url, description }); }); if (media.length) { - return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + const uploads: NewMediaItemResult[] = await PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + return uploads; } }; diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index d58c02ce5..348f216a5 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -74,13 +74,12 @@ export default class DirectoryImportBox extends React.Component handleSelection = async (e: React.ChangeEvent) => { runInAction(() => this.uploading = true); - let promises: Promise[] = []; let docs: Doc[] = []; let files = e.target.files; if (!files || files.length === 0) return; - let directory = (files.item(0) as any).webkitRelativePath.split("/", 1); + let directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0]; let validated: File[] = []; for (let i = 0; i < files.length; i++) { @@ -117,8 +116,6 @@ export default class DirectoryImportBox extends React.Component console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`); })); - await GooglePhotos.Transactions.UploadImages(docs, { title: directory }); - for (let i = 0; i < docs.length; i++) { let doc = docs[i]; doc.size = sizes[i]; @@ -142,11 +139,11 @@ export default class DirectoryImportBox extends React.Component let parent = this.props.ContainingCollectionView; if (parent) { let importContainer = Docs.Create.StackingDocument(docs, options); + await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); importContainer.singleColumn = false; Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); DocumentManager.Instance.jumpToDocument(importContainer, true); - } runInAction(() => { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8d10a91ce..28edf181b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -130,7 +130,7 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - // this.executeGooglePhotosRoutine(); + this.executeGooglePhotosRoutine(); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -149,14 +149,15 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } - // executeGooglePhotosRoutine = async () => { - // let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - // let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); - // doc.caption = "Well isn't this a nice cat image!"; - // let photos = await GooglePhotos.endpoint(); - // let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; - // console.log(await GooglePhotos.UploadImages([doc], { id: albumId })); - // } + executeGooglePhotosRoutine = async () => { + // let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + // let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + // doc.caption = "Well isn't this a nice cat image!"; + // let photos = await GooglePhotos.endpoint(); + // let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; + // console.log(await GooglePhotos.UploadImages([doc], { id: albumId })); + GooglePhotos.Query.Search({ included: [GooglePhotos.ContentCategories.ANIMALS] }).then(console.log); + } componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a51f783ad..a38f42751 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -594,7 +594,8 @@ export class DocumentView extends DocComponent(Docu cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); } if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum(this.props.Document).then(console.log), icon: "caret-square-right" }); + cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.AppendImageMetadata(this.props.Document), icon: "caret-square-right" }); } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 27737782b..6afe4ddfd 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -298,7 +298,7 @@ export namespace RichTextUtils { const length = node.nodeSize; const attrs = node.attrs; const uri = attrs.src; - const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([uri]); + const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([Docs.Create.ImageDocument(uri)]); if (!baseUrls) { continue; } diff --git a/src/server/apis/google/CustomizedWrapper/filters.js b/src/server/apis/google/CustomizedWrapper/filters.js new file mode 100644 index 000000000..576a90b75 --- /dev/null +++ b/src/server/apis/google/CustomizedWrapper/filters.js @@ -0,0 +1,46 @@ +'use strict'; + +const DateFilter = require('../common/date_filter'); +const MediaTypeFilter = require('./media_type_filter'); +const ContentFilter = require('./content_filter'); + +class Filters { + constructor(includeArchivedMedia = false) { + this.includeArchivedMedia = includeArchivedMedia; + } + + setDateFilter(dateFilter) { + this.dateFilter = dateFilter; + return this; + } + + setContentFilter(contentFilter) { + this.contentFilter = contentFilter; + return this; + } + + setMediaTypeFilter(mediaTypeFilter) { + this.mediaTypeFilter = mediaTypeFilter; + return this; + } + + setIncludeArchivedMedia(includeArchivedMedia) { + this.includeArchivedMedia = includeArchivedMedia; + return this; + } + + toJSON() { + return { + dateFilter: this.dateFilter instanceof DateFilter ? this.dateFilter.toJSON() : this.dateFilter, + mediaTypeFilter: this.mediaTypeFilter instanceof MediaTypeFilter ? + this.mediaTypeFilter.toJSON() : + this.mediaTypeFilter, + contentFilter: this.contentFilter instanceof ContentFilter ? + this.contentFilter.toJSON() : + this.contentFilter, + includeArchivedMedia: this.includeArchivedMedia + }; + } +} + +module.exports = Filters; \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 447ed23ac..1a8adc836 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -5,7 +5,7 @@ import { Utils } from '../../../Utils'; import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; import * as sharp from 'sharp'; -import { MediaItemCreationResult } from './SharedTypes'; +import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes'; const uploadDirectory = path.join(__dirname, "../../public/files/"); @@ -55,24 +55,34 @@ export namespace GooglePhotosUploadUtils { - export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }): Promise => { - return new Promise((resolve, reject) => { + export const CreateMediaItems = async (newMediaItems: any[], album?: { id: string }): Promise => { + const quota = newMediaItems.length; + let handled = 0; + const newMediaItemResults: NewMediaItemResult[] = []; + while (handled < quota) { + const cap = Math.min(newMediaItems.length, handled + 50); + const batch = newMediaItems.slice(handled, cap); + console.log(batch.length); const parameters = { method: 'POST', headers: headers('json'), uri: prepend('mediaItems:batchCreate'), - body: { newMediaItems } as any, + body: { newMediaItems: batch } as any, json: true }; album && (parameters.body.albumId = album.id); - request(parameters, (error, _response, body) => { - if (error) { - reject(error); - } else { - resolve(body); - } - }); - }); + newMediaItemResults.push(...(await new Promise((resolve, reject) => { + request(parameters, (error, _response, body) => { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + })).newMediaItemResults); + handled = cap; + } + return { newMediaItemResults }; }; } diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 22d57d744..0c06f68b7 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyAB5T3dgJqWuYBcLaT94wQo7MZkmzJQZxDB2sSU95mdhW24E3diuFdLeNsUDVI57D3S765RweMnL98d-fdgu1dRxpzkV_J_3rLih99pZ8A4d6jVdm1354UT4py_w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568161931458} \ No newline at end of file +{"access_token":"ya29.GlyAB7VxfbK7fwV9-lqu9NZ1-p73aC8KaEXAYGHFOIIgAhx40CCUgS07vy485y7O0x9RwK-7FL6P547SscD5bVlTlJkclP-9uupKxDaeez7Tc7o2pJwt6bgJlbbw7w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568220636395} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 2c3e76c55..507463841 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -831,7 +831,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { return _error(res, tokenError); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - mediaItems => _success(res, mediaItems), + result => _success(res, result.newMediaItemResults), error => _error(res, mediaError, error) ); }); -- cgit v1.2.3-70-g09d2 From 15c3a0fac7795ed07bd282571c477655d5f24327 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 11 Sep 2019 16:21:53 -0400 Subject: added back link to google photos --- deploy/assets/google_photos.png | Bin 0 -> 116940 bytes src/client/views/nodes/ImageBox.scss | 108 +++++++++++++++----------- src/client/views/nodes/ImageBox.tsx | 20 ++++- src/server/credentials/google_docs_token.json | 2 +- 4 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 deploy/assets/google_photos.png (limited to 'src/server') diff --git a/deploy/assets/google_photos.png b/deploy/assets/google_photos.png new file mode 100644 index 000000000..383cd410f Binary files /dev/null and b/deploy/assets/google_photos.png differ diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 00c069e1f..98cf7f92f 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,43 +1,59 @@ .imageBox-cont { - padding: 0vw; - position: relative; - text-align: center; - width: 100%; - height: auto; - max-width: 100%; - max-height: 100%; - pointer-events: none; + padding: 0vw; + position: relative; + text-align: center; + width: 100%; + height: auto; + max-width: 100%; + max-height: 100%; + pointer-events: none; } + .imageBox-cont-interactive { - pointer-events: all; - width:100%; - height:auto; + pointer-events: all; + width: 100%; + height: auto; } .imageBox-dot { - position:absolute; + position: absolute; bottom: 10; left: 0; border-radius: 10px; - width:20px; - height:20px; - background:gray; + width: 20px; + height: 20px; + background: gray; } .imageBox-cont img { height: auto; - width:100%; + width: 100%; } + .imageBox-cont-interactive img { height: auto; - width:100%; + width: 100%; +} + +#google-photos { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + top: 15px; + right: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; + cursor: pointer; } .imageBox-button { - padding: 0vw; - border: none; - width: 100%; - height: 100%; + padding: 0vw; + border: none; + width: 100%; + height: 100%; } .imageBox-audioBackground { @@ -49,6 +65,7 @@ border-radius: 25px; background: white; opacity: 0.3; + svg { width: 90% !important; height: 70%; @@ -59,44 +76,47 @@ } #cf { - position:relative; - width:100%; - margin:0 auto; - display:flex; + position: relative; + width: 100%; + margin: 0 auto; + display: flex; align-items: center; - height:100%; - overflow:hidden; + height: 100%; + overflow: hidden; + .imageBox-fadeBlocker { - width:100%; - height:100%; + width: 100%; + height: 100%; background: black; - display:flex; + display: flex; flex-direction: row; align-items: center; z-index: 1; + .imageBox-fadeaway { object-fit: contain; - width:100%; - height:100%; + width: 100%; + height: 100%; } } - } - - #cf img { - position:absolute; - left:0; - } - - .imageBox-fadeBlocker { +} + +#cf img { + position: absolute; + left: 0; +} + +.imageBox-fadeBlocker { -webkit-transition: opacity 1s ease-in-out; -moz-transition: opacity 1s ease-in-out; -o-transition: opacity 1s ease-in-out; transition: opacity 1s ease-in-out; - } - .imageBox-fadeBlocker:hover { +} + +.imageBox-fadeBlocker:hover { -webkit-transition: opacity 1s ease-in-out; -moz-transition: opacity 1s ease-in-out; -o-transition: opacity 1s ease-in-out; transition: opacity 1s ease-in-out; - opacity:0; - } \ No newline at end of file + opacity: 0; +} \ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6fc94a140..b7aadcd3d 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -63,11 +63,10 @@ export class ImageBox extends DocComponent(ImageD private _lastTap: number = 0; @observable private _isOpen: boolean = false; private dropDisposer?: DragManager.DragDropDisposer; - + @observable private hoverActive = false; @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { this.dropDisposer(); @@ -372,6 +371,20 @@ export class ImageBox extends DocComponent(ImageD this.recordAudioAnnotation(); } + considerGooglePhotosLink = () => { + const remoteUrl = StrCast(this.props.Document.googlePhotosUrl); + if (remoteUrl) { + return ( + window.open(remoteUrl)} + /> + ); + } + return (null); + } render() { // let transform = this.props.ScreenToLocalTransform().inverse(); @@ -408,6 +421,8 @@ export class ImageBox extends DocComponent(ImageD return (
this.hoverActive = true)} + onPointerLeave={action(() => this.hoverActive = false)} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
(ImageD
+ {this.considerGooglePhotosLink()} {/* {this.lightbox(paths)} */}
); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 0c06f68b7..c5026e60f 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyAB7VxfbK7fwV9-lqu9NZ1-p73aC8KaEXAYGHFOIIgAhx40CCUgS07vy485y7O0x9RwK-7FL6P547SscD5bVlTlJkclP-9uupKxDaeez7Tc7o2pJwt6bgJlbbw7w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568220636395} \ No newline at end of file +{"access_token":"ya29.GlyAB0qt-0gbGYiOwleSnxXDKKHo8k5Djr5VOlbioTfUNbzHzRrguj4fHiauxPNEesgQjBssx5djYipTHtzheoLaRiR8uHZ9bcz8RHsQYIaAW4QpvTQkwnLjGwkG5w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568235681891} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 5af7c8c709c8413239fe8642208891c2413dad62 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 11 Sep 2019 17:27:47 -0400 Subject: text enrichment and collections storing album id --- .../apis/google_docs/GooglePhotosClientUtils.ts | 14 +++++++++ src/client/views/nodes/DocumentView.tsx | 1 + src/server/apis/google/GooglePhotosUploadUtils.ts | 35 ++++++++++++---------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 2 +- 5 files changed, 36 insertions(+), 18 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 118462778..49eb5b354 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -107,6 +107,7 @@ export namespace GooglePhotos { if (tag) { await Query.TagChildImages(collection); } + collection.albumId = id; return { albumId: id, mediaItems }; } }; @@ -257,6 +258,19 @@ export namespace GooglePhotos { baseUrl: string; } + export const AddTextEnrichment = async (collection: Doc, content?: string) => { + const photos = await endpoint(); + const albumId = StrCast(collection.albumId); + if (albumId && albumId.length) { + const enrichment = new photos.TextEnrichment(content || Utils.prepend("/doc/" + collection[Id])); + const position = new photos.AlbumPosition(photos.AlbumPosition.POSITIONS.FIRST_IN_ALBUM); + const enrichmentItem = await photos.albums.addEnrichment(albumId, enrichment, position); + if (enrichmentItem) { + return enrichmentItem.id; + } + } + }; + export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise => { const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, body); return uploads; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fdec84526..1e4216dbb 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -596,6 +596,7 @@ export class DocumentView extends DocComponent(Docu if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 1a8adc836..7f47259db 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -103,7 +103,8 @@ export namespace DownloadUtils { const png = ".png"; const pngs = [".png", ".PNG"]; const jpgs = [".jpg", ".JPG", ".jpeg", ".JPEG"]; - const formats = [".jpg", ".png", ".gif"]; + const imageFormats = [".jpg", ".png", ".gif"]; + const videoFormats = [".mov", ".mp4"]; const size = "content-length"; const type = "content-type"; @@ -150,26 +151,28 @@ export namespace DownloadUtils { resizers.forEach(element => element.resizer = element.resizer.png()); } else if (jpgs.includes(extension)) { resizers.forEach(element => element.resizer = element.resizer.jpeg()); - } else if (!formats.includes(extension.toLowerCase())) { - return reject(); + } else if (![...imageFormats, ...videoFormats].includes(extension.toLowerCase())) { + return resolve(undefined); } - for (let resizer of resizers) { - const suffix = resizer.suffix; - let mediaPath: string; - await new Promise(resolve => { - const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; - information.mediaPaths.push(mediaPath = uploadDirectory + filename); - information.fileNames[suffix] = filename; - stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) - .on('close', resolve) - .on('error', reject); - }); - if (!isLocal) { + if (imageFormats.includes(extension)) { + for (let resizer of resizers) { + const suffix = resizer.suffix; + let mediaPath: string; await new Promise(resolve => { - stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve); + const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; + information.mediaPaths.push(mediaPath = uploadDirectory + filename); + information.fileNames[suffix] = filename; + stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) + .on('close', resolve) + .on('error', reject); }); } } + if (!isLocal) { + await new Promise(resolve => { + stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve); + }); + } resolve(information); }); }; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index c5026e60f..c58287bee 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyAB0qt-0gbGYiOwleSnxXDKKHo8k5Djr5VOlbioTfUNbzHzRrguj4fHiauxPNEesgQjBssx5djYipTHtzheoLaRiR8uHZ9bcz8RHsQYIaAW4QpvTQkwnLjGwkG5w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568235681891} \ No newline at end of file +{"access_token":"ya29.ImCBBwOPA7RqPIoh9RrZn90HLJnYAazRjts5R17yNQi9QLENQiChUUIUjcsTqbL-4cs_TK7UbEID6pR0w71gyTjVnA5uBcPJFcAaZ-GRPtheXx0PDU4oqSWHYoqlNQQKjn4","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568239483409} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 507463841..388c8cd4d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -579,7 +579,7 @@ app.post( for (const key in files) { const { type, path: location, name } = files[key]; const filename = path.basename(location); - await UploadUtils.UploadImage(uploadDirectory + filename, filename); + await UploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`)); results.push({ name, type, path: `/files/${filename}` }); console.log(path.basename(name)); } -- cgit v1.2.3-70-g09d2 From 2dd8b13fd3fa30fc390251ed75da3207efed4d5b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 11 Sep 2019 21:49:56 -0400 Subject: restored labels to pivot viewer --- src/client/apis/google_docs/GooglePhotosClientUtils.ts | 17 +++++++++++------ .../collectionFreeForm/CollectionFreeFormView.tsx | 13 +++++++------ src/server/apis/google/GooglePhotosUploadUtils.ts | 8 +++++++- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 2 +- 5 files changed, 27 insertions(+), 15 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 49eb5b354..700c0401a 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -135,13 +135,15 @@ export namespace GooglePhotos { export namespace Query { + const delimiter = ", "; export const TagChildImages = async (collection: Doc) => { const idMapping = await Cast(collection.googlePhotosIdMapping, Doc); if (!idMapping) { throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!"); } + const tagMapping = new Map(); const images = await DocListCastAsync(collection.data); - images && images.forEach(image => image.googlePhotosTags = new List([ContentCategories.NONE])); + images && images.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); const values = Object.values(ContentCategories); for (let value of values) { if (value !== ContentCategories.NONE) { @@ -151,9 +153,10 @@ export namespace GooglePhotos { for (let id of ids) { const image = await Cast(idMapping[id], Doc); if (image) { - const tags = Cast(image.googlePhotosTags, listSpec("string"))!; + const key = image[Id]; + const tags = tagMapping.get(key)!; if (!tags.includes(value)) { - tags.push(value); + tagMapping.set(key, tags + delimiter + value); } } } @@ -161,9 +164,11 @@ export namespace GooglePhotos { } } images && images.forEach(image => { - const tags = Cast(image.googlePhotosTags, listSpec("string"))!; - if (tags.includes(ContentCategories.NONE) && tags.length > 1) { - image.googlePhotosTags = new List(tags.splice(tags.indexOf(ContentCategories.NONE), 1)); + const concatenated = tagMapping.get(image[Id])!; + const tags = concatenated.split(delimiter); + if (tags.length > 1) { + const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, ""); + image.googlePhotosTags = cleaned.split(delimiter).sort((a, b) => (a < b) ? -1 : (a > b ? 1 : 0)).join(delimiter); } }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a7acd9e91..1af534ecd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -98,7 +98,7 @@ export namespace PivotView { groups.forEach((val, key) => minSize = Math.min(minSize, val.length)); const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize)); - const fontSize = NumCast(collection.pivotFontSize); + const fontSize = NumCast(collection.pivotFontSize, 30); const docMap = new Map(); const groupNames: PivotData[] = []; @@ -113,7 +113,8 @@ export namespace PivotView { x, y: width + 50, width: width * 1.25 * numCols, - height: 100, fontSize: fontSize + height: 100, + fontSize }); for (const doc of val) { docMap.set(doc, { @@ -701,7 +702,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result; } - viewDefsToJSX = (views: any[]) => { + viewDefsToJSX = (views: PivotView.PivotData[]) => { let elements: ViewDefResult[] = []; if (Array.isArray(views)) { elements = views.reduce((prev, ele) => { @@ -713,12 +714,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return elements; } - private viewDefToJSX(viewDef: any): Opt { + private viewDefToJSX(viewDef: PivotView.PivotData): Opt { if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); const x = Cast(viewDef.x, "number"); const y = Cast(viewDef.y, "number"); - const z = Cast(viewDef.z, "number"); + // const z = Cast(viewDef.z, "number"); const width = Cast(viewDef.width, "number"); const height = Cast(viewDef.height, "number"); const fontSize = Cast(viewDef.fontSize, "number"); @@ -730,7 +731,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ele:
{text}
, bounds: { x: x!, y: y!, z: z, width: width!, height: height! } + }}>{text}, bounds: { x: x!, y: y!, width: width!, height: height! } }; } } diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 7f47259db..51642e345 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; import * as sharp from 'sharp'; import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes'; +import { reject } from 'bluebird'; const uploadDirectory = path.join(__dirname, "../../public/files/"); @@ -50,7 +51,12 @@ export namespace GooglePhotosUploadUtils { uri: prepend('uploads'), body }; - return new Promise(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body))); + return new Promise(resolve => request(parameters, (error, _response, body) => { + if (error) { + return reject(error); + } + resolve(body); + })); }; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index c58287bee..7442c643a 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.ImCBBwOPA7RqPIoh9RrZn90HLJnYAazRjts5R17yNQi9QLENQiChUUIUjcsTqbL-4cs_TK7UbEID6pR0w71gyTjVnA5uBcPJFcAaZ-GRPtheXx0PDU4oqSWHYoqlNQQKjn4","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568239483409} \ No newline at end of file +{"access_token":"ya29.GlyBB9xlRimL3pw4tgNg7g7wcr73JWyQd4-XZbgOvngFM_sYUgsWP0YV7XCez5u6nytEfrOm228Sadj52wluJ46cJGhj2IwtSbW9GYzHHiiD-ts0i1phIV3n28wo5A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568254634977} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 388c8cd4d..9a2bd9a3a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -821,7 +821,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { const media: GooglePhotosUploadUtils.MediaInput[] = req.body.media; await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); const newMediaItems = await Promise.all(media.map(async element => { - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url).catch(error => _error(res, tokenError, error)); return !uploadToken ? undefined : { description: element.description, simpleMediaItem: { uploadToken } -- cgit v1.2.3-70-g09d2 From cbb016dd4bec4ce1367314717adf85640ae51c93 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 12 Sep 2019 05:21:29 -0400 Subject: sharing workflow supported --- .vscode/launch.json | 5 +- src/client/DocServer.ts | 10 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 3 +- src/client/documents/Documents.ts | 1 - src/client/util/DictationManager.ts | 46 +- src/client/util/History.ts | 6 +- src/client/util/SharingManager.scss | 136 +++ src/client/util/SharingManager.tsx | 293 ++++++ src/client/views/GlobalKeyHandler.ts | 2 + src/client/views/InkingCanvas.scss | 2 +- src/client/views/Main.scss | 40 - src/client/views/Main.tsx | 16 +- src/client/views/MainView.tsx | 272 ++++-- src/client/views/MainViewModal.scss | 25 + src/client/views/MainViewModal.tsx | 44 + src/client/views/OverlayView.tsx | 3 + src/client/views/ScriptingRepl.scss | 1 + .../views/collections/CollectionDockingView.tsx | 33 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 18 +- src/client/views/nodes/DocumentView.scss | 15 + src/client/views/nodes/DocumentView.tsx | 43 +- .../views/presentationview/PresentationView.tsx | 993 +++++++++++++++++++++ src/server/Message.ts | 7 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 2 - .../authentication/models/current_user_utils.ts | 8 +- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 64 +- 27 files changed, 1835 insertions(+), 255 deletions(-) create mode 100644 src/client/util/SharingManager.scss create mode 100644 src/client/util/SharingManager.tsx create mode 100644 src/client/views/MainViewModal.scss create mode 100644 src/client/views/MainViewModal.tsx create mode 100644 src/client/views/presentationview/PresentationView.tsx (limited to 'src/server') diff --git a/.vscode/launch.json b/.vscode/launch.json index d2c18d6f1..e1c5c6f94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,14 +3,13 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", - "configurations": [ - { + "configurations": [{ "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", "sourceMaps": true, "breakOnLoad": true, - "url": "http://localhost:1050/login", + "url": "http://localhost:1050/logout", "webRoot": "${workspaceFolder}", "runtimeArgs": [ "--experimental-modules" diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 2cec1046b..4dea4f11c 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -144,7 +144,7 @@ export namespace DocServer { * the server if the document has not been cached. * @param id the id of the requested document */ - const _GetRefFieldImpl = (id: string): Promise> => { + const _GetRefFieldImpl = (id: string, mongoCollection?: string): Promise> => { // an initial pass through the cache to determine whether the document needs to be fetched, // is already in the process of being fetched or already exists in the // cache @@ -155,7 +155,7 @@ export namespace DocServer { // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) // field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of // the field has been returned from the server - const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, id); + const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, { id, mongoCollection }); // when the serialized RefField has been received, go head and begin deserializing it into an object. // Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all @@ -188,10 +188,10 @@ export namespace DocServer { } }; - let _GetRefField: (id: string) => Promise> = errorFunc; + let _GetRefField: (id: string, mongoCollection?: string) => Promise> = errorFunc; - export function GetRefField(id: string): Promise> { - return _GetRefField(id); + export function GetRefField(id: string, mongoCollection = "newDocuments"): Promise> { + return _GetRefField(id, mongoCollection); } export async function getYoutubeChannels() { diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 700c0401a..b308cc9be 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -108,6 +108,7 @@ export namespace GooglePhotos { await Query.TagChildImages(collection); } collection.albumId = id; + Transactions.AddTextEnrichment(collection, `Find me at ${Utils.prepend(`/doc/${collection[Id]}?sharing=true`)}`); return { albumId: id, mediaItems }; } }; @@ -313,7 +314,7 @@ export namespace GooglePhotos { }; const parseDescription = (document: Doc, descriptionKey: string) => { - let description: string = Utils.prepend("/doc/" + document[Id]); + let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`); const target = document[descriptionKey]; if (typeof target === "string") { description = target; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5dd945c16..cfed2bf14 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -158,7 +158,6 @@ export namespace Docs { [DocumentType.LINKDOC, { data: new List(), layout: { view: EmptyBox }, - options: {} }], [DocumentType.YOUTUBE, { layout: { view: YoutubeBox } diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index fb3c15cea..0711effe6 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -3,7 +3,7 @@ import { DocumentView } from "../views/nodes/DocumentView"; import { UndoManager } from "./UndoManager"; import * as interpreter from "words-to-numbers"; import { DocumentType } from "../documents/DocumentTypes"; -import { Doc } from "../../new_fields/Doc"; +import { Doc, Opt } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; import { Docs } from "../documents/Documents"; import { CollectionViewType } from "../views/collections/CollectionBaseView"; @@ -40,12 +40,26 @@ export namespace DictationManager { webkitSpeechRecognition: any; } } - const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow; + const { webkitSpeechRecognition }: CORE.IWindow = window as any as CORE.IWindow; export const placeholder = "Listening..."; export namespace Controls { export const Infringed = "unable to process: dictation manager still involved in previous session"; + const browser = (() => { + let identifier = navigator.userAgent.toLowerCase(); + if (identifier.indexOf("safari") >= 0) { + return "Safari"; + } + if (identifier.indexOf("chrome") >= 0) { + return "Chrome"; + } + if (identifier.indexOf("firefox") >= 0) { + return "Firefox"; + } + return "Unidentified Browser"; + })(); + const unsupported = `listening is not supported in ${browser}`; const intraSession = ". "; const interSession = " ... "; @@ -55,8 +69,7 @@ export namespace DictationManager { let current: string | undefined = undefined; let sessionResults: string[] = []; - const recognizer: SpeechRecognition = new webkitSpeechRecognition() || new SpeechRecognition(); - recognizer.onstart = () => console.log("initiating speech recognition session..."); + const recognizer: Opt = webkitSpeechRecognition ? new webkitSpeechRecognition() : undefined; export type InterimResultHandler = (results: string) => any; export type ContinuityArgs = { indefinite: boolean } | false; @@ -109,6 +122,10 @@ export namespace DictationManager { }; const listenImpl = (options?: Partial) => { + if (!recognizer) { + console.log(unsupported); + return unsupported; + } if (isListening) { return Infringed; } @@ -121,6 +138,7 @@ export namespace DictationManager { let intra = options && options.delimiters ? options.delimiters.intra : undefined; let inter = options && options.delimiters ? options.delimiters.inter : undefined; + recognizer.onstart = () => console.log("initiating speech recognition session..."); recognizer.interimResults = handler !== undefined; recognizer.continuous = continuous === undefined ? false : continuous !== false; recognizer.lang = language === undefined ? "en-US" : language; @@ -167,14 +185,20 @@ export namespace DictationManager { } else { resolve(current); } - reset(); + current = undefined; + sessionResults = []; + isListening = false; + isManuallyStopped = false; + recognizer.onresult = null; + recognizer.onerror = null; + recognizer.onend = null; }; }); }; export const stop = (salvageSession = true) => { - if (!isListening) { + if (!isListening || !recognizer) { return; } isManuallyStopped = true; @@ -197,16 +221,6 @@ export namespace DictationManager { return transcripts.join(delimiter || intraSession); }; - const reset = () => { - current = undefined; - sessionResults = []; - isListening = false; - isManuallyStopped = false; - recognizer.onresult = null; - recognizer.onerror = null; - recognizer.onend = null; - }; - } export namespace Commands { diff --git a/src/client/util/History.ts b/src/client/util/History.ts index e9ff21b22..c72ae05de 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -16,8 +16,10 @@ export namespace HistoryUtil { initializers?: { [docId: string]: DocInitializerList; }; + safe?: boolean; readonly?: boolean; nro?: boolean; + sharing?: boolean; } export type ParsedUrl = DocUrl; @@ -141,7 +143,7 @@ export namespace HistoryUtil { }; } - addParser("doc", {}, { readonly: true, initializers: true, nro: true }, (pathname, opts, current) => { + addParser("doc", {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => { if (pathname.length !== 2) return undefined; current.initializers = current.initializers || {}; @@ -156,7 +158,7 @@ export namespace HistoryUtil { export function parseUrl(location: Location | URL): ParsedUrl | undefined { const pathname = location.pathname.substring(1); const search = location.search; - const opts = qs.parse(search, { sort: false }); + const opts = search.length ? qs.parse(search, { sort: false }) : {}; let pathnameSplit = pathname.split("/"); const type = pathnameSplit[0]; diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss new file mode 100644 index 000000000..9a4c5db30 --- /dev/null +++ b/src/client/util/SharingManager.scss @@ -0,0 +1,136 @@ +.sharing-interface { + display: flex; + flex-direction: column; + + p { + font-size: 20px; + text-align: left; + font-style: italic; + padding: 0; + margin: 0 0 20px 0; + } + + .hr-substitute { + border: solid black 0.5px; + margin-top: 20px; + } + + .people-with-container { + display: flex; + height: 25px; + + .people-with { + font-size: 14px; + margin: 0; + padding-top: 3px; + font-style: normal; + } + + .people-with-select { + width: 126px; + outline: none; + } + } + + .share-individual { + margin-top: 20px; + margin-bottom: 20px; + } + + .users-list { + font-style: italic; + background: white; + border: 1px solid black; + padding-left: 10px; + padding-right: 10px; + max-height: 200px; + overflow: scroll; + height: -webkit-fill-available; + text-align: left; + display: flex; + align-content: center; + align-items: center; + text-align: center; + justify-content: center; + color: red; + } + + .container { + display: block; + position: relative; + margin-top: 10px; + margin-bottom: 10px; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 700px; + min-width: 700px; + max-width: 700px; + text-align: left; + font-style: normal; + font-size: 15; + font-weight: normal; + padding: 0; + + .padding { + padding: 0 0 0 20px; + color: black; + } + + .permissions-dropdown { + outline: none; + } + } + + .no-users { + margin-top: 20px; + } + + .link-container { + display: flex; + flex-direction: row; + margin-bottom: 10px; + margin-left: auto; + margin-right: auto; + + .link-box, + .copy { + padding: 10px; + border-radius: 10px; + padding: 10px; + border: solid black 1px; + } + + .link-box { + background: white; + color: blue; + text-decoration: underline; + } + + .copy { + margin-left: 20px; + cursor: alias; + border-radius: 50%; + width: 42px; + height: 42px; + transition: 1.5s all ease; + padding-top: 12px; + } + } + + .close-button { + border-radius: 5px; + margin-top: 20px; + padding: 10px 0; + background: aliceblue; + transition: 0.5s ease all; + border: 1px solid; + border-color: aliceblue; + } + + .close-button:hover { + border-color: black; + } +} \ No newline at end of file diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx new file mode 100644 index 000000000..72a4b4141 --- /dev/null +++ b/src/client/util/SharingManager.tsx @@ -0,0 +1,293 @@ +import { observable, runInAction, action, autorun } from "mobx"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { Doc, Opt } from "../../new_fields/Doc"; +import { DocServer } from "../DocServer"; +import { Cast, StrCast } from "../../new_fields/Types"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { RouteStore } from "../../server/RouteStore"; +import * as RequestPromise from "request-promise"; +import { Utils } from "../../Utils"; +import "./SharingManager.scss"; +import { Id } from "../../new_fields/FieldSymbols"; +import { observer } from "mobx-react"; +import { MainView } from "../views/MainView"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import * as fa from '@fortawesome/free-solid-svg-icons'; +import { DocumentView } from "../views/nodes/DocumentView"; +import { SelectionManager } from "./SelectionManager"; +import { DocumentManager } from "./DocumentManager"; +import { CollectionVideoView } from "../views/collections/CollectionVideoView"; +import { CollectionPDFView } from "../views/collections/CollectionPDFView"; +import { CollectionView } from "../views/collections/CollectionView"; + +library.add(fa.faCopy); + +export interface User { + email: string; + userDocumentId: string; +} + +export enum SharingPermissions { + None = "Not Shared", + View = "Can View", + Comment = "Can Comment", + Edit = "Can Edit" +} + +const ColorMapping = new Map([ + [SharingPermissions.None, "red"], + [SharingPermissions.View, "maroon"], + [SharingPermissions.Comment, "blue"], + [SharingPermissions.Edit, "green"] +]); + +const SharingKey = "sharingPermissions"; +const PublicKey = "publicLinkPermissions"; +const DefaultColor = "black"; + +@observer +export default class SharingManager extends React.Component<{}> { + public static Instance: SharingManager; + @observable private isOpen = false; + @observable private users: User[] = []; + @observable private targetDoc: Doc | undefined; + @observable private targetDocView: DocumentView | undefined; + @observable private copied = false; + @observable private dialogueBoxOpacity = 1; + @observable private overlayOpacity = 0.4; + + private get linkVisible() { + return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; + } + + public open = (target: DocumentView) => { + SelectionManager.DeselectAll(); + this.populateUsers().then(action(() => { + this.targetDocView = target; + this.targetDoc = target.props.Document; + MainView.Instance.hasActiveModal = true; + this.isOpen = true; + if (!this.sharingDoc) { + this.sharingDoc = new Doc; + } + })); + } + + public close = action(() => { + this.isOpen = false; + setTimeout(action(() => { + this.copied = false; + MainView.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), 500); + }); + + private get sharingDoc() { + return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined; + } + + private set sharingDoc(value: Doc | undefined) { + this.targetDoc && (this.targetDoc[SharingKey] = value); + } + + constructor(props: {}) { + super(props); + SharingManager.Instance = this; + } + + populateUsers = async () => { + let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers)); + runInAction(() => { + this.users = (JSON.parse(userList) as User[]).filter(({ email }) => email !== CurrentUserUtils.email); + }); + } + + setInternalSharing = async (user: User, state: string) => { + if (!this.sharingDoc) { + console.log("SHARING ABORTED!"); + return; + } + let sharingDoc = await this.sharingDoc; + sharingDoc[user.userDocumentId] = state; + const userDocument = await DocServer.GetRefField(user.userDocumentId); + if (!(userDocument instanceof Doc)) { + console.log(`Couldn't get user document of user ${user.email}`); + return; + } + let target = this.targetDoc; + if (!target) { + console.log("SharingManager trying to share an undefined document!!"); + return; + } + const notifDoc = await Cast(userDocument.optionalRightCollection, Doc); + if (notifDoc instanceof Doc) { + const data = await Cast(notifDoc.data, listSpec(Doc)); + if (!data) { + console.log("UNABLE TO ACCESS NOTIFICATION DATA"); + return; + } + console.log(`Attempting to set permissions to ${state} for the document ${target[Id]}`); + if (state !== SharingPermissions.None) { + const sharedDoc = Doc.MakeAlias(target); + if (data) { + data.push(sharedDoc); + } else { + notifDoc.data = new List([sharedDoc]); + } + } else { + let dataDocs = (await Promise.all(data.map(doc => doc))).map(doc => Doc.GetProto(doc)); + if (dataDocs.includes(target)) { + console.log("Searching in ", dataDocs, "for", target); + dataDocs.splice(dataDocs.indexOf(target), 1); + console.log("SUCCESSFULLY UNSHARED DOC"); + } else { + console.log("DIDN'T THINK WE HAD IT, SO NOT SUCCESSFULLY UNSHARED"); + } + } + } + } + + private setExternalSharing = (state: string) => { + let sharingDoc = this.sharingDoc; + if (!sharingDoc) { + return; + } + sharingDoc[PublicKey] = state; + } + + private get sharingUrl() { + if (!this.targetDoc) { + return undefined; + } + let baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]); + return `${baseUrl}?sharing=true`; + } + + copy = action(() => { + if (this.sharingUrl) { + Utils.CopyText(this.sharingUrl); + this.copied = true; + } + }); + + private get sharingOptions() { + return Object.values(SharingPermissions).map(permission => { + return ( + + ); + }); + } + + private focusOn = (contents: string) => { + let title = this.targetDoc ? StrCast(this.targetDoc.title) : ""; + return ( + { + let context: Opt; + if (this.targetDoc && this.targetDocView && (context = this.targetDocView.props.ContainingCollectionView)) { + DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, undefined, undefined, context.props.Document); + } + }} + onPointerEnter={action(() => { + if (this.targetDoc) { + Doc.BrushDoc(this.targetDoc); + this.dialogueBoxOpacity = 0.1; + this.overlayOpacity = 0.1; + } + })} + onPointerLeave={action(() => { + this.targetDoc && Doc.UnBrushDoc(this.targetDoc); + this.dialogueBoxOpacity = 1; + this.overlayOpacity = 0.4; + })} + > + {contents} + + ); + } + + private get sharingInterface() { + return ( +
+

Manage the public link to {this.focusOn("this document...")}

+ {!this.linkVisible ? (null) : +
+
{this.sharingUrl}
+
+ +
+
+ } +
+ {!this.linkVisible ? (null) :

People with this link

} + +
+
+

Privately share {this.focusOn("this document")} with an individual...

+
+ {!this.users.length ? "There are no other users in your database." : + this.users.map(user => { + return ( +
+ + {user.email} +
+ ); + }) + } +
+
Done
+
+ ); + } + + render() { + return ( + + ); + } + +} \ No newline at end of file diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d0464bd5f..0255ab78a 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -6,6 +6,7 @@ import { DragManager } from "../util/DragManager"; import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DictationManager } from "../util/DictationManager"; +import SharingManager from "../util/SharingManager"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise; @@ -72,6 +73,7 @@ export default class KeyManager { main.toggleColorPicker(true); SelectionManager.DeselectAll(); DictationManager.Controls.stop(); + SharingManager.Instance.close(); break; case "delete": case "backspace": diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss index 5437b26d6..1365974dd 100644 --- a/src/client/views/InkingCanvas.scss +++ b/src/client/views/InkingCanvas.scss @@ -34,7 +34,7 @@ .inkingCanvas-noSelect { pointer-events: none; - cursor: "arrow"; + cursor: "crosshair"; } .inkingCanvas-paths-ink, diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index bc0975c86..04249506a 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -268,44 +268,4 @@ ul#add-options-list { height: 25%; position: relative; display: flex; -} - -.dictation-prompt { - position: absolute; - z-index: 1000; - text-align: center; - justify-content: center; - align-self: center; - align-content: center; - padding: 20px; - background: gainsboro; - border-radius: 10px; - border: 3px solid black; - box-shadow: #00000044 5px 5px 10px; - transform: translate(-50%, -50%); - top: 50%; - font-style: italic; - left: 50%; - transition: 0.5s all ease; - pointer-events: none; -} - -.dictation-prompt-overlay { - width: 100%; - height: 100%; - position: absolute; - z-index: 999; - transition: 0.5s all ease; - pointer-events: none; -} - -.webpage-input { - display: none; - height: 60px; - width: 600px; - position: absolute; - - .url-input { - width: 80%; - } } \ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index e35ba18e4..b623cab4e 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -52,12 +52,16 @@ let swapDocs = async () => { const info = await CurrentUserUtils.loadCurrentUser(); DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email); await Docs.Prototypes.initialize(); - await CurrentUserUtils.loadUserDocument(info); - // updates old user documents to prevent chrome on tree view. - (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled"; - (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled"; - (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled"; - await swapDocs(); + if (info.id !== "__guest__") { + // a guest will not have an id registered + await CurrentUserUtils.loadUserDocument(info); + // updates old user documents to prevent chrome on tree view. + (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled"; + (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled"; + (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled"; + CurrentUserUtils.UserDocument.chromeStatus = "disabled"; + await swapDocs(); + } document.getElementById('root')!.addEventListener('wheel', event => { if (event.ctrlKey) { event.preventDefault(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 28edf181b..85bf0344b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -7,8 +7,8 @@ import "normalize.css"; import * as React from 'react'; import { SketchPicker } from 'react-color'; import Measure from 'react-measure'; -import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc'; import { List } from '../../new_fields/List'; +import { Doc, DocListCast, Opt, HeightSym, FieldResult, Field } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; import { InkTool } from '../../new_fields/InkField'; import { listSpec } from '../../new_fields/Schema'; @@ -17,14 +17,14 @@ import { CurrentUserUtils } from '../../server/authentication/models/current_use import { RouteStore } from '../../server/RouteStore'; import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils'; import { DocServer } from '../DocServer'; -import { Docs } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; import { DictationManager } from '../util/DictationManager'; import { SetupDrag } from '../util/DragManager'; -import { HistoryUtil } from '../util/History'; import { Transform } from '../util/Transform'; import { UndoManager, undoBatch } from '../util/UndoManager'; -import { CollectionBaseView } from './collections/CollectionBaseView'; +import { Docs, DocumentOptions } from '../documents/Documents'; +import { HistoryUtil } from '../util/History'; +import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionTreeView } from './collections/CollectionTreeView'; import { ContextMenu } from './ContextMenu'; @@ -44,6 +44,9 @@ import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../new_fields/URLField'; import { LinkFollowBox } from './linking/LinkFollowBox'; import { DocumentManager } from '../util/DocumentManager'; +import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; +import MainViewModal from './MainViewModal'; +import SharingManager from '../util/SharingManager'; @observer export class MainView extends React.Component { @@ -57,6 +60,8 @@ export class MainView extends React.Component { @observable private dictationDisplayState = false; @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false; + public hasActiveModal = false; + public overlayTimeout: NodeJS.Timeout | undefined; public initiateDictationFade = () => { @@ -64,10 +69,17 @@ export class MainView extends React.Component { this.overlayTimeout = setTimeout(() => { this.dictationOverlayVisible = false; this.dictationSuccess = undefined; + this.hasActiveModal = false; setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500); }, duration); } + private urlState: HistoryUtil.DocUrl; + + @computed private get userDoc() { + return CurrentUserUtils.UserDocument; + } + public cancelDictationFade = () => { if (this.overlayTimeout) { clearTimeout(this.overlayTimeout); @@ -76,7 +88,7 @@ export class MainView extends React.Component { } @computed private get mainContainer(): Opt { - return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); + return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } @computed get mainFreeform(): Opt { let docs = DocListCast(this.mainContainer!.data); @@ -85,7 +97,10 @@ export class MainView extends React.Component { public isPointerDown = false; private set mainContainer(doc: Opt) { if (doc) { - CurrentUserUtils.UserDocument.activeWorkspace = doc; + if (!("presentationView" in doc)) { + doc.presentationView = new List([Docs.Create.TreeDocument([], { title: "Presentation" })]); + } + this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc); } } @@ -130,23 +145,23 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - this.executeGooglePhotosRoutine(); - - reaction(() => { - let workspaces = CurrentUserUtils.UserDocument.workspaces; - let recent = CurrentUserUtils.UserDocument.recentlyClosed; - if (!(recent instanceof Doc)) return 0; - if (!(workspaces instanceof Doc)) return 0; - let workspacesDoc = workspaces; - let recentDoc = recent; - let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + CurrentUserUtils.UserDocument[HeightSym]() * 0.00001; - return libraryHeight; - }, (libraryHeight: number) => { - if (libraryHeight && Math.abs(CurrentUserUtils.UserDocument[HeightSym]() - libraryHeight) > 5) { - CurrentUserUtils.UserDocument.height = libraryHeight; - } - (Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc).allowClear = true; - }, { fireImmediately: true }); + if (this.userDoc) { + reaction(() => { + let workspaces = this.userDoc.workspaces; + let recent = this.userDoc.recentlyClosed; + if (!(recent instanceof Doc)) return 0; + if (!(workspaces instanceof Doc)) return 0; + let workspacesDoc = workspaces; + let recentDoc = recent; + let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + this.userDoc[HeightSym]() * 0.00001; + return libraryHeight; + }, (libraryHeight: number) => { + if (libraryHeight && Math.abs(this.userDoc[HeightSym]() - libraryHeight) > 5) { + this.userDoc.height = libraryHeight; + } + (Cast(this.userDoc.recentlyClosed, Doc) as Doc).allowClear = true; + }, { fireImmediately: true }); + } } executeGooglePhotosRoutine = async () => { @@ -169,7 +184,7 @@ export class MainView extends React.Component { constructor(props: Readonly<{}>) { super(props); MainView.Instance = this; - + this.urlState = HistoryUtil.parseUrl(window.location) || {} as any; // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); if (window.location.pathname !== RouteStore.home) { @@ -178,6 +193,12 @@ export class MainView extends React.Component { let type = pathname[0]; if (type === "doc") { CurrentUserUtils.MainDocId = pathname[1]; + if (!this.userDoc) { + runInAction(() => this.flyoutWidth = 0); + DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action(field => { + field instanceof Doc && (CurrentUserUtils.GuestTarget = field); + })); + } } } } @@ -234,68 +255,109 @@ export class MainView extends React.Component { initAuthenticationRouters = async () => { // Load the user's active workspace, or create a new one if initial session after signup - if (!CurrentUserUtils.MainDocId) { - const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc); - if (doc) { + let received = CurrentUserUtils.MainDocId; + if (received && !this.userDoc) { + reaction( + () => CurrentUserUtils.GuestTarget, + target => target && this.createNewWorkspace(), + { fireImmediately: true } + ); + } else { + if (received && this.urlState.sharing) { + reaction( + () => { + let docking = CollectionDockingView.Instance; + return docking && docking.initialized; + }, + initialized => { + if (initialized && received) { + DocServer.GetRefField(received).then(field => { + if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) { + const target = Doc.MakeAlias(field); + const artificialParent = Docs.Create.FreeformDocument([target], { title: `View of ${StrCast(field.title)}` }); + CollectionDockingView.Instance.AddRightSplit(artificialParent, undefined); + DocumentManager.Instance.jumpToDocument(target, true, undefined, undefined, undefined, artificialParent); + } + }); + } + }, + ); + } + let doc: Opt; + if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) { this.openWorkspace(doc); } else { this.createNewWorkspace(); } - } else { - DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => - field instanceof Doc ? this.openWorkspace(field) : - this.createNewWorkspace(CurrentUserUtils.MainDocId)); } } - @action createNewWorkspace = async (id?: string) => { - let workspaces = Cast(CurrentUserUtils.UserDocument.workspaces, Doc); - if (!(workspaces instanceof Doc)) return; - const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc)); - if (list) { - let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` }); - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] }; - let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id); - if (!CurrentUserUtils.UserDocument.linkManagerDoc) { - let linkManagerDoc = new Doc(); - linkManagerDoc.allLinks = new List([]); - CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc; + let freeformOptions: DocumentOptions = { + x: 0, + y: 400, + width: this.pwidth * .7, + height: this.pheight, + title: CurrentUserUtils.GuestTarget ? `Guest View of ${StrCast(CurrentUserUtils.GuestTarget.title)}` : "My Blank Collection" + }; + let workspaces: FieldResult; + let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] }; + let mainDoc = Docs.Create.DockDocument([this.userDoc, freeformDoc], JSON.stringify(dockingLayout), {}, id); + if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) { + const list = Cast((workspaces).data, listSpec(Doc)); + if (list) { + if (!this.userDoc.linkManagerDoc) { + let linkManagerDoc = new Doc(); + linkManagerDoc.allLinks = new List([]); + this.userDoc.linkManagerDoc = linkManagerDoc; + } + list.push(mainDoc); + mainDoc.title = `Workspace ${list.length}`; } - list.push(mainDoc); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => { - this.openWorkspace(mainDoc); - // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" }); - // mainDoc.optionalRightCollection = pendingDocument; - }, 0); } + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + this.openWorkspace(mainDoc); + // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" }); + // mainDoc.optionalRightCollection = pendingDocument; + }, 0); } @action openWorkspace = async (doc: Doc, fromHistory = false) => { CurrentUserUtils.MainDocId = doc[Id]; this.mainContainer = doc; - const state = HistoryUtil.parseUrl(window.location) || {} as any; - fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], readonly: state.readonly, nro: state.nro }); - if (state.readonly === true || state.readonly === null) { + let state = this.urlState; + if (state.sharing === true && !this.userDoc) { DocServer.Control.makeReadOnly(); - } else if (state.safe) { - if (!state.nro) { + } else { + fromHistory || HistoryUtil.pushState({ + type: "doc", + docId: doc[Id], + readonly: state.readonly, + nro: state.nro, + sharing: false, + }); + if (state.readonly === true || state.readonly === null) { + DocServer.Control.makeReadOnly(); + } else if (state.safe) { + if (!state.nro) { + DocServer.Control.makeReadOnly(); + } + CollectionBaseView.SetSafeMode(true); + } else if (state.nro || state.nro === null || state.readonly === false) { + } else if (BoolCast(doc.readOnly)) { DocServer.Control.makeReadOnly(); + } else { + DocServer.Control.makeEditable(); } - CollectionBaseView.SetSafeMode(true); - } else if (state.nro || state.nro === null || state.readonly === false) { - } else if (BoolCast(doc.readOnly)) { - DocServer.Control.makeReadOnly(); - } else { - DocServer.Control.makeEditable(); } - const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc); + let col: Opt; // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) setTimeout(async () => { - if (col) { + if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) { const l = Cast(col.data, listSpec(Doc)); if (l) { runInAction(() => CollectionTreeView.NotifsCol = col); @@ -389,11 +451,12 @@ export class MainView extends React.Component { } @computed get flyout() { - let sidebar = CurrentUserUtils.UserDocument.sidebar; - if (!(sidebar instanceof Doc)) return (null); - let sidebarDoc = sidebar; + let sidebar: FieldResult; + if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) { + return (null); + } return + if (!this.userDoc) { + return
{this.dockingContent}
; + } + let sidebar = this.userDoc.sidebar; + if (!(sidebar instanceof Doc)) { + return (null); + } + return
@@ -448,14 +516,22 @@ export class MainView extends React.Component { } } - toggleLinkFollowBox = (shouldClose: boolean) => { - if (LinkFollowBox.Instance) { - let dvs = DocumentManager.Instance.getDocumentViews(LinkFollowBox.Instance.props.Document); - // if it already exisits, close it - LinkFollowBox.Instance.props.Document.isMinimized = (dvs.length > 0 && shouldClose); - } + setWriteMode = (mode: DocServer.WriteMode) => { + console.log(DocServer.WriteMode[mode]); + const mode1 = mode; + const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; + DocServer.setFieldWriteMode("x", mode1); + DocServer.setFieldWriteMode("y", mode1); + DocServer.setFieldWriteMode("width", mode1); + DocServer.setFieldWriteMode("height", mode1); + + DocServer.setFieldWriteMode("panX", mode2); + DocServer.setFieldWriteMode("panY", mode2); + DocServer.setFieldWriteMode("scale", mode2); + DocServer.setFieldWriteMode("viewType", mode2); } + @observable private _colorPickerDisplay = false; /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ nodesMenu() { @@ -501,7 +577,13 @@ export class MainView extends React.Component {
)} -
  • +
  • + {ClientUtils.RELEASE ? [] : [ +
  • , +
  • , +
  • , +
  • + ]}
  • ; + } else { + return ; + } + } + + //The function that starts or resets presentaton functionally, depending on status flag. + @action + startOrResetPres = async () => { + if (this.presStatus) { + this.resetPresentation(); + } else { + this.presStatus = true; + let startIndex = await this.findStartDocument(); + this.startPresentation(startIndex); + const current = NumCast(this.curPresentation.selectedDoc); + this.gotoDocument(startIndex, current); + } + this.curPresentation.presStatus = this.presStatus; + } + + /** + * This method is called to find the start document of presentation. So + * that when user presses on play, the correct presentation element will be + * selected. + */ + findStartDocument = async () => { + let docAtZero = await this.getDocAtIndex(0); + if (docAtZero === undefined) { + return 0; + } + let docAtZeroPresId = StrCast(docAtZero.presentId); + + if (this.groupMappings.has(docAtZeroPresId)) { + let group = this.groupMappings.get(docAtZeroPresId)!; + let lastDoc = group[group.length - 1]; + return this.childrenDocs.indexOf(lastDoc); + } else { + return 0; + } + } + + //The function that resets the presentation by removing every action done by it. It also + //stops the presentaton. + @action + resetPresentation = () => { + this.childrenDocs.forEach((doc: Doc) => { + doc.opacity = 1; + doc.viewScale = 1; + }); + this.curPresentation.selectedDoc = 0; + this.presStatus = false; + this.curPresentation.presStatus = this.presStatus; + if (this.childrenDocs.length === 0) { + return; + } + DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1); + } + + + //The function that starts the presentation, also checking if actions should be applied + //directly at start. + startPresentation = (startIndex: number) => { + let selectedButtons: boolean[]; + this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => { + selectedButtons = component.selected; + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(doc) > startIndex) { + doc.opacity = 0; + } + + } + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(doc) < startIndex) { + doc.opacity = 0; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(doc) < startIndex) { + doc.opacity = 0.5; + } + } + + }); + + } + + /** + * The function that is called to add a new presentation to the presentationView. + * It sets up te mappings and local copies of it. Resets the groupings and presentation. + * Makes the new presentation current selected, and retrieve the back-Ups if present. + */ + @action + addNewPresentation = (presTitle: string) => { + //creating a new presentation doc + let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle }); + this.props.Documents.push(newPresentationDoc); + + //setting that new doc as current + this.curPresentation = newPresentationDoc; + + //storing the doc in local copies for easier access + let newGuid = Utils.GenerateGuid(); + this.presentationsMapping.set(newGuid, newPresentationDoc); + this.presentationsKeyMapping.set(newPresentationDoc, newGuid); + + //resetting the previous presentation's actions so that new presentation can be loaded. + this.resetGroupIds(); + this.resetPresentation(); + this.presElementsMappings = new Map(); + this.currentSelectedPresValue = newGuid; + this.setPresentationBackUps(); + + } + + /** + * The function that is called to change the current selected presentation. + * Changes the presentation, also resetting groupings and presentation in process. + * Plus retrieving the backUps for the newly selected presentation. + */ + @action + getSelectedPresentation = (e: React.ChangeEvent) => { + //get the guid of the selected presentation + let selectedGuid = e.target.value; + //set that as current presentation + this.curPresentation = this.presentationsMapping.get(selectedGuid)!; + + //reset current Presentations local things so that new one can be loaded + this.resetGroupIds(); + this.resetPresentation(); + this.presElementsMappings = new Map(); + this.currentSelectedPresValue = selectedGuid; + this.setPresentationBackUps(); + + + } + + /** + * The function that is called to render either select for presentations, or title inputting. + */ + renderSelectOrPresSelection = () => { + let presentationList = DocListCast(this.props.Documents); + if (this.PresTitleInputOpen || this.PresTitleChangeOpen) { + return this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />; + } else { + return ; + } + } + + /** + * The function that is called on enter press of title input. It gives the + * new presentation the title user entered. If nothing is entered, gives a default title. + */ + @action + submitPresentationTitle = (e: React.KeyboardEvent) => { + if (e.keyCode === 13) { + let presTitle = this.titleInputElement!.value; + this.titleInputElement!.value = ""; + if (this.PresTitleInputOpen) { + if (presTitle === "") { + presTitle = "Presentation"; + } + this.PresTitleInputOpen = false; + this.addNewPresentation(presTitle); + } else if (this.PresTitleChangeOpen) { + this.PresTitleChangeOpen = false; + this.changePresentationTitle(presTitle); + } + } + } + + /** + * The function that is called to remove a presentation from all its copies, and the main Container's + * list. Sets up the next presentation as current. + */ + @action + removePresentation = async () => { + if (this.presentationsMapping.size !== 1) { + let presentationList = Cast(this.props.Documents, listSpec(Doc)); + let batch = UndoManager.StartBatch("presRemoval"); + + //getting the presentation that will be removed + let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!); + //that presentation is removed + presentationList!.splice(presentationList!.indexOf(removedDoc!), 1); + + //its mappings are removed from local copies + this.presentationsKeyMapping.delete(removedDoc!); + this.presentationsMapping.delete(this.currentSelectedPresValue!); + + //the next presentation is set as current + let remainingPresentations = this.presentationsMapping.values(); + let nextDoc = remainingPresentations.next().value; + this.curPresentation = nextDoc; + + + //Storing these for being able to undo changes + let curGuid = this.currentSelectedPresValue!; + let curPresStatus = this.presStatus; + + //resetting the groups and presentation actions so that next presentation gets loaded + this.resetGroupIds(); + this.resetPresentation(); + this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); + this.setPresentationBackUps(); + + //Storing for undo + let currentGroups = this.groupMappings; + let curPresElemMapping = this.presElementsMappings; + + //Event to undo actions that are not related to doc directly, aka. local things + UndoManager.AddEvent({ + undo: action(() => { + this.curPresentation = removedDoc!; + this.presentationsMapping.set(curGuid, removedDoc!); + this.presentationsKeyMapping.set(removedDoc!, curGuid); + this.currentSelectedPresValue = curGuid; + + this.presStatus = curPresStatus; + this.groupMappings = currentGroups; + this.presElementsMappings = curPresElemMapping; + this.setPresentationBackUps(); + + }), + redo: action(() => { + this.curPresentation = nextDoc; + this.presStatus = false; + this.presentationsKeyMapping.delete(removedDoc!); + this.presentationsMapping.delete(curGuid); + this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); + this.setPresentationBackUps(); + + }), + }); + + batch.end(); + } + } + + /** + * The function that is called to change title of presentation to what user entered. + */ + @undoBatch + changePresentationTitle = (newTitle: string) => { + if (newTitle === "") { + return; + } + this.curPresentation.title = newTitle; + } + + /** + * On pointer down element that is catched on resizer of te + * presentation view. Sets up the event listeners to change the size with + * mouse move. + */ + _downsize = 0; + onPointerDown = (e: React.PointerEvent) => { + this._downsize = e.clientX; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + e.preventDefault(); + } + /** + * Changes the size of the presentation view, with mouse move. + * Minimum size is set to 300, so that every button is visible. + */ + @action + onPointerMove = (e: PointerEvent) => { + + this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth); + } + + /** + * The method that is called on pointer up event. It checks if the button is just + * clicked so that presentation view will be closed. The way it's done is to check + * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger. + */ + @action + onPointerUp = (e: PointerEvent) => { + if (Math.abs(e.clientX - this._downsize) < 4) { + let presWidth = NumCast(this.curPresentation.width); + if (presWidth - presMinWidth !== 0) { + this.curPresentation.width = 0; + } + if (presWidth === 0) { + this.curPresentation.width = presMinWidth; + } + } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + /** + * This function is a setter that opens up the + * presentation mode, by setting it's render flag + * to true. It also closes the presentation view. + */ + @action + openPresMode = () => { + if (!this.presMode) { + this.curPresentation.width = 0; + this.presMode = true; + } + } + + /** + * This function closes the presentation mode by setting its + * render flag to false. It also opens up the presentation view. + * By setting it to it's minimum size. + */ + @action + closePresMode = () => { + if (this.presMode) { + this.presMode = false; + this.curPresentation.width = presMinWidth; + } + + } + + /** + * Function that is called to render the presentation mode, depending on its flag. + */ + renderPresMode = () => { + if (this.presMode) { + return ; + } else { + return (null); + } + + } + + render() { + + let width = NumCast(this.curPresentation.width); + + return ( +
    +
    !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}> +
    + {this.renderSelectOrPresSelection()} + + + + + +
    +
    + + {this.renderPlayPauseButton()} + +
    + + this.presElementsMappings.clear()} + /> + ) => { + this.persistOpacity = e.target.checked; + this.opacity = this.persistOpacity ? 1 : 0.4; + })} + checked={this.persistOpacity} + style={{ position: "absolute", bottom: 5, left: 5 }} + onPointerEnter={action(() => this.labelOpacity = 1)} + onPointerLeave={action(() => this.labelOpacity = 0)} + /> +

    opacity {this.persistOpacity ? "persistent" : "on focus"}

    +
    +
    + +
    + {this.renderPresMode()} + +
    + ); + } +} diff --git a/src/server/Message.ts b/src/server/Message.ts index 4ec390ade..a5679797f 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -23,6 +23,7 @@ export interface Transferable { readonly id: string; readonly type: Types; readonly data?: any; + readonly mongoCollection?: string; } export enum YoutubeQueryTypes { @@ -43,6 +44,10 @@ export interface Diff extends Reference { readonly diff: any; } +export interface SourceSpecified extends Reference { + readonly mongoCollection?: string; +} + export namespace MessageStore { export const Foo = new Message("Foo"); export const Bar = new Message("Bar"); @@ -52,7 +57,7 @@ export namespace MessageStore { export const GetDocument = new Message("Get Document"); export const DeleteAll = new Message("Delete All"); - export const GetRefField = new Message("Get Ref Field"); + export const GetRefField = new Message("Get Ref Field"); export const GetRefFields = new Message("Get Ref Fields"); export const UpdateField = new Message("Update Ref Field"); export const CreateField = new Message("Create Ref Field"); diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index c2656cc1c..0215c533f 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -58,8 +58,6 @@ export namespace GooglePhotosUploadUtils { })); }; - - export const CreateMediaItems = async (newMediaItems: any[], album?: { id: string }): Promise => { const quota = newMediaItems.length; let handled = 0; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index af5774ebe..050a71eb4 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -2,7 +2,6 @@ import { action, computed, observable, runInAction } from "mobx"; import * as rp from 'request-promise'; import { DocServer } from "../../../client/DocServer"; import { Docs } from "../../../client/documents/Documents"; -import { Gateway, NorthstarSettings } from "../../../client/northstar/manager/Gateway"; import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea"; import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView"; @@ -24,6 +23,9 @@ export class CurrentUserUtils { public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } + @observable public static GuestTarget: Doc | undefined; + @observable public static GuestWorkspace: Doc | undefined; + private static createUserDocument(id: string): Doc { let doc = new Doc(id, true); doc.viewType = CollectionViewType.Tree; @@ -59,7 +61,7 @@ export class CurrentUserUtils { noteTypes.excludeFromLibrary = true; doc.noteTypes = noteTypes; } - PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(vals => DocListCast(vals))); + PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast)); if (doc.recentlyClosed === undefined) { const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 }); recentlyClosed.excludeFromLibrary = true; @@ -112,7 +114,7 @@ export class CurrentUserUtils { this.curr_id = id; Doc.CurrentUserEmail = email; await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => { - if (id) { + if (id && id !== "guest") { return DocServer.GetRefField(id).then(async field => { if (field instanceof Doc) { await this.updateUserDocument(field); diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index fec1625f5..31763c2cf 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyBB1MlCG7GL2pYFleLp9uUJoN6s0_PFBDLUIhyrKAY4kkVo7vbuaW_zmkJs1Fym0f7NVpaYvFsBK2dbN6Qn5P8bWNW2NsHNNGcwbyGIS8H52GUlyCsawNt6PTnOw","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568274162450} \ No newline at end of file +{"access_token":"ya29.GlyBB9YYhy7l9LZ9yDpItKvLpibt59SpmBQUMo_sX-3d4eN8W-9teuc_7Ca4YiOboy_gHTdcwaR1ArnpQEqZlzOsfNmV6dXZsldgxin3bVuDn1q4sCWvz01yuZduIA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568281677559} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 101a4f63f..62c3df8de 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -21,10 +21,10 @@ import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; import { Utils } from '../Utils'; import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller'; -import { DashUserModel } from './authentication/models/user_model'; +import User, { DashUserModel } from './authentication/models/user_model'; import { Client } from './Client'; import { Database } from './database'; -import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message"; +import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput, SourceSpecified } from "./Message"; import { RouteStore } from './RouteStore'; import v4 = require('uuid/v4'); const app = express(); @@ -36,9 +36,7 @@ const serverPort = 4321; import expressFlash = require('express-flash'); import flash = require('connect-flash'); import { Search } from './Search'; -import _ = require('lodash'); import * as Archiver from 'archiver'; -import * as request_promise from 'request-promise'; var AdmZip = require('adm-zip'); import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; @@ -47,6 +45,7 @@ import { GooglePhotosUploadUtils, DownloadUtils as UploadUtils } from './apis/go const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); +import * as qs from 'query-string'; const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -113,7 +112,9 @@ function addSecureRoute(method: Method, ...subscribers: string[] ) { let abstracted = (req: express.Request, res: express.Response) => { - if (req.user) { + let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true"; + sharing = sharing && req.originalUrl.startsWith("/doc/"); + if (req.user || sharing) { handler(req.user, res, req); } else { req.session!.target = req.originalUrl; @@ -507,21 +508,20 @@ addSecureRoute( res.sendFile(path.join(__dirname, '../../deploy/' + filename)); }, undefined, - RouteStore.home, - RouteStore.openDocumentWithId + RouteStore.home, RouteStore.openDocumentWithId ); addSecureRoute( Method.GET, - (user, res) => res.send(user.userDocumentId || ""), - undefined, + (user, res) => res.send(user.userDocumentId), + (res) => res.send(undefined), RouteStore.getUserDocumentId, ); addSecureRoute( Method.GET, - (user, res) => res.send(JSON.stringify({ id: user.id, email: user.email })), - undefined, + (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); }, + (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })), RouteStore.getCurrUser ); @@ -666,21 +666,31 @@ app.use(RouteStore.corsProxy, (req, res) => { }).pipe(res); }); -app.get(RouteStore.delete, (req, res) => { - if (release) { - res.send("no"); - return; - } - deleteFields().then(() => res.redirect(RouteStore.home)); -}); +addSecureRoute( + Method.GET, + (user, res, req) => { + if (release) { + res.send("no"); + return; + } + deleteFields().then(() => res.redirect(RouteStore.home)); + }, + undefined, + RouteStore.delete +); -app.get(RouteStore.deleteAll, (req, res) => { - if (release) { - res.send("no"); - return; - } - deleteAll().then(() => res.redirect(RouteStore.home)); -}); +addSecureRoute( + Method.GET, + (user, res, req) => { + if (release) { + res.send("no"); + return; + } + deleteAll().then(() => res.redirect(RouteStore.home)); + }, + undefined, + RouteStore.deleteAll +); app.use(wdm(compiler, { publicPath: config.output.publicPath })); @@ -766,8 +776,8 @@ function setField(socket: Socket, newValue: Transferable) { } } -function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { - Database.Instance.getDocument(id, callback, "newDocuments"); +function GetRefField([args, callback]: [SourceSpecified, (result?: Transferable) => void]) { + Database.Instance.getDocument(args.id, callback, args.mongoCollection || "newDocuments"); } function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { -- cgit v1.2.3-70-g09d2 From cb04cae3e5b7d4ae3fb2e59afe866d95320aab14 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 12 Sep 2019 14:38:09 -0400 Subject: now support custom view --- src/client/apis/google_docs/GooglePhotosClientUtils.ts | 2 +- src/client/views/MainView.tsx | 6 ++---- src/server/credentials/google_docs_token.json | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 63cbc8867..3dac1d65c 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -306,7 +306,7 @@ export namespace GooglePhotos { return; } const url = data.url.href; - const description = parseDescription(source, descriptionKey); + const description = parseDescription(Doc.MakeAlias(source), descriptionKey); media.push({ url, description }); }); if (media.length) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 85bf0344b..f7b66cae3 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -273,10 +273,8 @@ export class MainView extends React.Component { if (initialized && received) { DocServer.GetRefField(received).then(field => { if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) { - const target = Doc.MakeAlias(field); - const artificialParent = Docs.Create.FreeformDocument([target], { title: `View of ${StrCast(field.title)}` }); - CollectionDockingView.Instance.AddRightSplit(artificialParent, undefined); - DocumentManager.Instance.jumpToDocument(target, true, undefined, undefined, undefined, artificialParent); + CollectionDockingView.Instance.AddRightSplit(field, undefined); + DocumentManager.Instance.jumpToDocument(field, true, undefined, undefined, undefined, undefined); } }); } diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index bb313f136..5b0b5ab5d 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyBB9xs77CFscdtWApHKMcsd6eS9NW3tO0FEvZlfO87HTl7zc1nIVhvtB7MLxadXvxVg4VUAvl6eFjVFsbdmA7TmURhIygYsZbds87ybMuLH5W68mRAVd3HDYyCzg","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568310697477} \ No newline at end of file +{"access_token":"ya29.GlyBB-8WTaj3RgOZt5lYaTgidUCgFXHwwtO1ZOYfo9gYq_YuAGQfVC-uRDJ36fIIEgi9F_TWgp8rda2MEXK4KCtTyeeG6Q8-03pdxEdCMdcgf01cmZbheErDY3iLEQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568316273289} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From ce85076e3cc4b14d7e9ff75a4562d479a0374d2f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 12 Sep 2019 16:49:47 -0400 Subject: auto custom --- .../apis/google_docs/GooglePhotosClientUtils.ts | 11 ++- src/client/views/TemplateMenu.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 91 +++++++++++----------- src/server/credentials/google_docs_token.json | 2 +- 4 files changed, 55 insertions(+), 53 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 3dac1d65c..f3f652ce1 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -14,6 +14,7 @@ import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/Share import { AssertionError } from "assert"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; +import { DocumentView } from "../../views/nodes/DocumentView"; export namespace GooglePhotos { @@ -97,10 +98,10 @@ export namespace GooglePhotos { } const idMapping = new Doc; for (let i = 0; i < images.length; i++) { - const image = images[i]; + const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; image.googlePhotosId = mediaItem.id; - image.googlePhotosUrl = mediaItem.baseUrl || mediaItem.productUrl; + image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; idMapping[mediaItem.id] = image; } collection.googlePhotosIdMapping = idMapping; @@ -143,7 +144,7 @@ export namespace GooglePhotos { throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!"); } const tagMapping = new Map(); - const images = await DocListCastAsync(collection.data); + const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); images && images.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); const values = Object.values(ContentCategories); for (let value of values) { @@ -306,7 +307,9 @@ export namespace GooglePhotos { return; } const url = data.url.href; - const description = parseDescription(Doc.MakeAlias(source), descriptionKey); + const target = Doc.MakeAlias(source); + const description = parseDescription(target, descriptionKey); + DocumentView.makeCustomViewClicked(target); media.push({ url, description }); }); if (media.length) { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 0586b31e4..4e371ffd1 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -57,9 +57,9 @@ export class TemplateMenu extends React.Component { toggleCustom = (e: React.MouseEvent): void => { this.props.docs.map(dv => { if (dv.Document.type !== DocumentType.COL && dv.Document.type !== DocumentType.TEMPLATE) { - dv.makeCustomViewClicked(); + DocumentView.makeCustomViewClicked(dv.props.Document); } else if (dv.Document.nativeLayout) { - dv.makeNativeViewClicked(); + DocumentView.makeNativeViewClicked(dv.props.Document); } }); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 6b305d179..81805af64 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -440,47 +440,6 @@ export class DocumentView extends DocComponent(Docu this.props.addDocTab(kvp, this.dataDoc, "onRight"); } - @undoBatch - makeNativeViewClicked = (): void => { - this.props.Document.customLayout = this.props.Document.layout; - this.props.Document.layout = this.props.Document.nativeLayout; - this.props.Document.type = this.props.Document.nativeType; - this.props.Document.nativeWidth = this.props.Document.nativeNativeWidth; - this.props.Document.nativeHeight = this.props.Document.nativeNativeHeight; - this.props.Document.ignoreAspect = this.props.Document.nativeIgnoreAspect; - this.props.Document.nativeLayout = undefined; - this.props.Document.nativeNativeWidth = undefined; - this.props.Document.nativeNativeHeight = undefined; - this.props.Document.nativeIgnoreAspect = undefined; - } - @undoBatch - makeCustomViewClicked = (): void => { - this.props.Document.nativeLayout = this.props.Document.layout; - this.props.Document.nativeType = this.props.Document.type; - this.props.Document.nativeNativeWidth = this.props.Document.nativeWidth; - this.props.Document.nativeNativeHeight = this.props.Document.nativeHeight; - this.props.Document.nativeIgnoreAspect = this.props.Document.ignoreAspect; - PromiseValue(Cast(this.props.Document.customLayout, Doc)).then(custom => { - if (custom) { - this.props.Document.type = DocumentType.TEMPLATE; - this.props.Document.layout = custom; - !custom.nativeWidth && (this.props.Document.nativeWidth = 0); - !custom.nativeHeight && (this.props.Document.nativeHeight = 0); - !custom.nativeWidth && (this.props.Document.ignoreAspect = true); - } else { - let options = { title: "data", width: NumCast(this.props.Document.width), height: NumCast(this.props.Document.height) + 25, x: -NumCast(this.props.Document.width) / 2, y: -NumCast(this.props.Document.height) / 2, }; - let fieldTemplate = this.props.Document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : Docs.Create.ImageDocument("http://www.cs.brown.edu", options); - - let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(this.Document.title) + "layout", width: NumCast(this.props.Document.width) + 20, height: Math.max(100, NumCast(this.props.Document.height) + 45) }); - let metaKey = "data"; - let proto = Doc.GetProto(docTemplate); - Doc.MakeTemplate(fieldTemplate, metaKey, proto); - - Doc.ApplyTemplateTo(docTemplate, this.props.Document, undefined, false); - } - }); - } - @undoBatch makeBtnClicked = (): void => { let doc = Doc.GetProto(this.props.Document); @@ -577,7 +536,7 @@ export class DocumentView extends DocComponent(Docu @action makeIntoPortal = (): void => { if (!DocListCast(this.props.Document.links).find(doc => { - if (Cast(doc.anchor2, Doc) instanceof Doc && (Cast(doc.anchor2, Doc) as Doc)!.title === this.props.Document.title + ".portal") return true; + if (Cast(doc.anchor2, Doc) instanceof Doc && (Cast(doc.anchor2, Doc) as Doc).title === this.props.Document.title + ".portal") return true; return false; })) { let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); @@ -611,6 +570,46 @@ export class DocumentView extends DocComponent(Docu }); } + public static makeNativeViewClicked = undoBatch((document: Doc): void => { + document.customLayout = document.layout; + document.layout = document.nativeLayout; + document.type = document.nativeType; + document.nativeWidth = document.nativeNativeWidth; + document.nativeHeight = document.nativeNativeHeight; + document.ignoreAspect = document.nativeIgnoreAspect; + document.nativeLayout = undefined; + document.nativeNativeWidth = undefined; + document.nativeNativeHeight = undefined; + document.nativeIgnoreAspect = undefined; + }); + + public static makeCustomViewClicked = undoBatch((document: Doc): void => { + document.nativeLayout = document.layout; + document.nativeType = document.type; + document.nativeNativeWidth = document.nativeWidth; + document.nativeNativeHeight = document.nativeHeight; + document.nativeIgnoreAspect = document.ignoreAspect; + PromiseValue(Cast(document.customLayout, Doc)).then(custom => { + if (custom) { + document.type = DocumentType.TEMPLATE; + document.layout = custom; + !custom.nativeWidth && (document.nativeWidth = 0); + !custom.nativeHeight && (document.nativeHeight = 0); + !custom.nativeWidth && (document.ignoreAspect = true); + } else { + let options = { title: "data", width: NumCast(document.width), height: NumCast(document.height) + 25, x: -NumCast(document.width) / 2, y: -NumCast(document.height) / 2, }; + let fieldTemplate = document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + + let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(document.title) + "layout", width: NumCast(document.width) + 20, height: Math.max(100, NumCast(document.height) + 45) }); + let metaKey = "data"; + let proto = Doc.GetProto(docTemplate); + Doc.MakeTemplate(fieldTemplate, metaKey, proto); + + Doc.ApplyTemplateTo(docTemplate, document, undefined, false); + } + }); + }); + @action onContextMenu = async (e: React.MouseEvent): Promise => { e.persist(); @@ -642,7 +641,7 @@ export class DocumentView extends DocComponent(Docu let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); - makes.push({ description: "Custom Document View", event: this.makeCustomViewClicked, icon: "concierge-bell" }); + makes.push({ description: "Custom Document View", event: () => DocumentView.makeCustomViewClicked(this.props.Document), icon: "concierge-bell" }); makes.push({ description: "Metadata Field View", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }); makes.push({ description: "Into Portal", event: this.makeIntoPortal, icon: "window-restore" }); makes.push({ description: this.layoutDoc.ignoreClick ? "Selectable" : "Unselectable", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" }); @@ -667,7 +666,7 @@ export class DocumentView extends DocComponent(Docu let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: this.props.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.layout instanceof Doc) { - layoutItems.push({ description: "Make View of Metadata Field", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }) + layoutItems.push({ description: "Make View of Metadata Field", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }); } layoutItems.push({ description: `${this.layoutDoc.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.layoutDoc.chromeStatus = (this.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); layoutItems.push({ description: `${this.layoutDoc.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" }); @@ -679,9 +678,9 @@ export class DocumentView extends DocComponent(Docu layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" }); } if (this.props.Document.type !== DocumentType.COL && this.props.Document.type !== DocumentType.TEMPLATE) { - layoutItems.push({ description: "Use Custom Layout", event: this.makeCustomViewClicked, icon: "concierge-bell" }); + layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document), icon: "concierge-bell" }); } else if (this.props.Document.nativeLayout) { - layoutItems.push({ description: "Use Native Layout", event: this.makeNativeViewClicked, icon: "concierge-bell" }); + layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); if (!ClientUtils.RELEASE) { diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 5b0b5ab5d..1f097346a 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyBB-8WTaj3RgOZt5lYaTgidUCgFXHwwtO1ZOYfo9gYq_YuAGQfVC-uRDJ36fIIEgi9F_TWgp8rda2MEXK4KCtTyeeG6Q8-03pdxEdCMdcgf01cmZbheErDY3iLEQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568316273289} \ No newline at end of file +{"access_token":"ya29.GlyBB937-mpLmukf1RrP8tQNfoWZvuHUjt0IxFuYfqNg1dHv1bBe04Tnc2CD_3p3qrtjjY5i2jUq--zaTf9_-CZi2TU2KnygPgDg4oyP5SgiHXv1pR0vlKRyNjhJqA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568322341079} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From f110a6cf1cac724a85e1001491e1bddedb8d1ebc Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 13 Sep 2019 13:01:21 -0400 Subject: indication that all images in a collection have been tagged --- deploy/assets/google_tags.png | Bin 0 -> 8093 bytes src/client/views/collections/CollectionBaseView.scss | 18 ++++++++++++++++-- src/client/views/collections/CollectionBaseView.tsx | 19 ++++++++++++++++++- src/client/views/nodes/ImageBox.scss | 13 +++++++++++++ src/client/views/nodes/ImageBox.tsx | 15 ++++++++++++++- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 1 - 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 deploy/assets/google_tags.png (limited to 'src/server') diff --git a/deploy/assets/google_tags.png b/deploy/assets/google_tags.png new file mode 100644 index 000000000..deb416407 Binary files /dev/null and b/deploy/assets/google_tags.png differ diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss index 583e6f6ca..aff965469 100644 --- a/src/client/views/collections/CollectionBaseView.scss +++ b/src/client/views/collections/CollectionBaseView.scss @@ -1,4 +1,5 @@ @import "../globalCssVariables"; + #collectionBaseView { border-width: 0; border-color: $light-color-secondary; @@ -6,7 +7,20 @@ border-radius: 0 0 $border-radius $border-radius; box-sizing: border-box; border-radius: inherit; - width:100%; - height:100%; + width: 100%; + height: 100%; overflow: auto; +} + +#google-tags { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + bottom: 15px; + left: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; } \ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index b7036b3ff..93eaab453 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { listSpec } from '../../../new_fields/Schema'; @@ -13,6 +13,7 @@ import { FieldViewProps } from '../nodes/FieldView'; import './CollectionBaseView.scss'; import { DateField } from '../../../new_fields/DateField'; import { DocumentType } from '../../documents/DocumentTypes'; +import { ImageField } from '../../../new_fields/URLField'; export enum CollectionViewType { Invalid, @@ -154,6 +155,21 @@ export class CollectionBaseView extends React.Component { return false; } + showIsTagged = () => { + const children = DocListCast(this.props.Document.data); + const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); + const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); + if (allTagged) { + return ( + + ); + } + return (null); + } + render() { const props: CollectionRenderProps = { addDocument: this.addDocument, @@ -171,6 +187,7 @@ export class CollectionBaseView extends React.Component { }} className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> + {this.showIsTagged()} {viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
  • ); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 98cf7f92f..71d718b39 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -49,6 +49,19 @@ cursor: pointer; } +#google-tags { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + bottom: 15px; + right: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; +} + .imageBox-button { padding: 0vw; border: none; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 515f968ab..649d2d056 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -219,7 +219,7 @@ export class ImageBox extends DocComponent(ImageD let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); - !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }) + !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); } @@ -387,6 +387,19 @@ export class ImageBox extends DocComponent(ImageD return (null); } + considerGooglePhotosTags = () => { + const tags = StrCast(this.props.Document.googlePhotosTags); + if (tags) { + return ( + + ); + } + return (null); + } + render() { // let transform = this.props.ScreenToLocalTransform().inverse(); let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 1f097346a..bdeca837b 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyBB937-mpLmukf1RrP8tQNfoWZvuHUjt0IxFuYfqNg1dHv1bBe04Tnc2CD_3p3qrtjjY5i2jUq--zaTf9_-CZi2TU2KnygPgDg4oyP5SgiHXv1pR0vlKRyNjhJqA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568322341079} \ No newline at end of file +{"access_token":"ya29.ImCCBwLh8M4qd5ApvvhgMeCvbQidOUehUNU2fj3RH6Zx8D3rnCooiVgxoWbJ2ddS3a0_PGAQvCA7-GAeS70wUny80VKgCLjNbTlZkuxaRqpAd5yFGuWzcRljXrEIuA7EVu0","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568394019509} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index d7273bd88..fdcc79b4d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -86,7 +86,6 @@ app.use(expressValidator()); app.use(passport.initialize()); app.use(passport.session()); app.use((req, res, next) => { - console.log(req.originalUrl); res.locals.user = req.user; next(); }); -- cgit v1.2.3-70-g09d2 From 3c2b04f16ccfae103e2f3acdd852e337c5f974e1 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 13 Sep 2019 17:11:25 -0400 Subject: added batching, generically --- src/client/northstar/utils/Extensions.ts | 27 ++++++++++ .../util/Import & Export/DirectoryImportBox.tsx | 33 ++++++------ src/client/util/UtilExtensions.ts | 39 +++++++++++++++ src/client/views/Main.tsx | 8 --- src/server/apis/google/GooglePhotosUploadUtils.ts | 58 +++++++++++----------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 53 ++++++++++++++++---- 7 files changed, 155 insertions(+), 65 deletions(-) create mode 100644 src/client/util/UtilExtensions.ts (limited to 'src/server') diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts index ab9384f1f..720f4a062 100644 --- a/src/client/northstar/utils/Extensions.ts +++ b/src/client/northstar/utils/Extensions.ts @@ -5,6 +5,8 @@ interface String { hasNewline(): boolean; } +const extensions = require(".././/.//../util/UtilExtensions"); + String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string { var target = this; return target.split(toReplace).join(replacement); @@ -18,6 +20,31 @@ String.prototype.Truncate = function (length: number, replacement: string): Stri return target; }; +interface Action { + handler: (batch: T[]) => any; + interval?: number; +} + +interface BatchParameters { + size: number; + action?: Action; +} + +interface Array { + batch(parameters: BatchParameters): Promise; + lastElement(): T; +} + +Array.prototype.batch = extensions.Batch; + +Array.prototype.lastElement = function () { + if (!this.length) { + return undefined; + } + const last: T = this[this.length - 1]; + return last; +}; + interface Math { log10(val: number): number; } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 260c6a629..5915f3412 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -92,29 +92,28 @@ export default class DirectoryImportBox extends React.Component this.completed = 0; }); - let sizes = []; - let modifiedDates = []; + let sizes: number[] = []; + let modifiedDates: number[] = []; - let i = 0; const uploads: FileResponse[] = []; - const batchSize = 15; - while (i < validated.length) { - const cap = Math.min(validated.length, i + batchSize); - let formData = new FormData(); - const batch = validated.slice(i, cap); + await validated.batch({ + size: 15, + action: { + handler: async (batch: File[]) => { + sizes.push(...batch.map(file => file.size)); + modifiedDates.push(...batch.map(file => file.lastModified)); - sizes.push(...batch.map(file => file.size)); - modifiedDates.push(...batch.map(file => file.lastModified)); + let formData = new FormData(); + batch.forEach(file => formData.append(Utils.GenerateGuid(), file)); + const parameters = { method: 'POST', body: formData }; - batch.forEach(file => formData.append(Utils.GenerateGuid(), file)); - const parameters = { method: 'POST', body: formData }; - uploads.push(...(await (await fetch(Utils.prepend(RouteStore.upload), parameters)).json())); - - runInAction(() => this.completed += batch.length); - i = cap; - } + uploads.push(...(await (await fetch(Utils.prepend(RouteStore.upload), parameters)).json())); + runInAction(() => this.completed += batch.length); + } + } + }); await Promise.all(uploads.map(async upload => { const type = upload.type; diff --git a/src/client/util/UtilExtensions.ts b/src/client/util/UtilExtensions.ts new file mode 100644 index 000000000..1e277b242 --- /dev/null +++ b/src/client/util/UtilExtensions.ts @@ -0,0 +1,39 @@ +module.exports.Batch = async function (parameters: BatchParameters) { + const { size, action } = parameters; + const batches: T[][] = []; + let i = 0; + while (i < this.length) { + const cap = Math.min(i + size, this.length); + batches.push(this.slice(i, cap)); + i = cap; + } + console.log(`Beginning action on ${this.length} elements, split into ${batches.length} groups => ${batches.map(batch => batch.length).join(", ")}`); + if (action) { + const { handler, interval } = action; + if (!interval || batches.length === 1) { + for (let batch of batches) { + await handler(batch); + } + } else { + return new Promise(resolve => { + const iterator = batches[Symbol.iterator](); + const quota = batches.length; + let completed = 0; + const tag = setInterval(async () => { + const next = iterator.next(); + if (next.done) { + clearInterval(tag); + return; + } + const batch = next.value; + console.log(`Handling next batch with ${batch.length} elements`); + await handler(batch); + if (++completed === quota) { + resolve(batches); + } + }, interval); + }); + } + } + return batches; +}; \ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b623cab4e..aa002cee9 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -20,14 +20,6 @@ String.prototype.hasNewline = function () { return this.endsWith("\n"); }; -(Array.prototype as any).lastElement = function (this: any[]) { - if (!this.length) { - return undefined; - } - return this[this.length - 1]; -}; - - let swapDocs = async () => { let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc); // Docs.Prototypes.MainLinkDocument().allLinks = new List(); diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index e91f8352b..3989590c6 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; import * as sharp from 'sharp'; import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes'; +import { NewMediaItem } from '../..'; const uploadDirectory = path.join(__dirname, "../../public/files/"); @@ -39,7 +40,7 @@ export namespace GooglePhotosUploadUtils { }; export const DispatchGooglePhotosUpload = async (url: string) => { - const body = await request(url, { encoding: null }); + const body = await request(url, { encoding: null }).catch(error => console.log("Error in streaming body!", error)); const parameters = { method: 'POST', headers: { @@ -56,36 +57,37 @@ export namespace GooglePhotosUploadUtils { return reject(error); } resolve(body); - })); + }).catch(error => console.log("Error in literal uploading process to Google's servers!", error))).catch(error => console.log("Error in literal uploading process to Google's servers!", error)); }; - export const CreateMediaItems = async (newMediaItems: any[], album?: { id: string }): Promise => { - const quota = newMediaItems.length; - let handled = 0; + export const CreateMediaItems = async (newMediaItems: NewMediaItem[], album?: { id: string }): Promise => { const newMediaItemResults: NewMediaItemResult[] = []; - while (handled < quota) { - const cap = Math.min(newMediaItems.length, handled + 50); - const batch = newMediaItems.slice(handled, cap); - console.log(batch.length); - const parameters = { - method: 'POST', - headers: headers('json'), - uri: prepend('mediaItems:batchCreate'), - body: { newMediaItems: batch } as any, - json: true - }; - album && (parameters.body.albumId = album.id); - newMediaItemResults.push(...(await new Promise((resolve, reject) => { - request(parameters, (error, _response, body) => { - if (error) { - reject(error); - } else { - resolve(body); - } - }); - })).newMediaItemResults); - handled = cap; - } + await newMediaItems.batch({ + size: 50, + action: { + handler: async (batch: NewMediaItem[]) => { + console.log(batch.length); + const parameters = { + method: 'POST', + headers: headers('json'), + uri: prepend('mediaItems:batchCreate'), + body: { newMediaItems: batch } as any, + json: true + }; + album && (parameters.body.albumId = album.id); + newMediaItemResults.push(...(await new Promise((resolve, reject) => { + request(parameters, (error, _response, body) => { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + })).newMediaItemResults); + }, + interval: 1000 + } + }); return { newMediaItemResults }; }; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index bdeca837b..d8e0eae21 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.ImCCBwLh8M4qd5ApvvhgMeCvbQidOUehUNU2fj3RH6Zx8D3rnCooiVgxoWbJ2ddS3a0_PGAQvCA7-GAeS70wUny80VKgCLjNbTlZkuxaRqpAd5yFGuWzcRljXrEIuA7EVu0","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568394019509} \ No newline at end of file +{"access_token":"ya29.GlyDB0wsV3-oS6q5TFuJSmH1YP_SPf_X6RHaJVmfqj0NTCtaPLFonZRxdT52kUkiHJgAoRizxZvlSIGptXKfnmG4BFouhgyo9ZKP0QtOH-kPR9b9x5WhGCd5NWqz0A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568412547334} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index fdcc79b4d..542a4ea65 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -46,6 +46,7 @@ const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); import * as qs from 'query-string'; +const extensions = require("../client/util/UtilExtensions"); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -827,20 +828,50 @@ app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.R const tokenError = "Unable to successfully upload bytes for all images!"; const mediaError = "Unable to convert all uploaded bytes to media items!"; +export interface NewMediaItem { + description: string; + simpleMediaItem: { + uploadToken: string; + }; +} + +Array.prototype.batch = extensions.Batch; + app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { - const media: GooglePhotosUploadUtils.MediaInput[] = req.body.media; + const mediaInput: GooglePhotosUploadUtils.MediaInput[] = req.body.media; await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); - const newMediaItems = await Promise.all(media.map(async element => { - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url).catch(error => { - console.log("Dispatching upload error!"); - console.log(error); + + const newMediaItems: NewMediaItem[] = []; + let failed = 0; + const size = 25; + + try { + await mediaInput.batch({ + size, + action: { + handler: async (batch: GooglePhotosUploadUtils.MediaInput[]) => { + await Promise.all(batch.map(async element => { + console.log(`Uploading ${element.url} to Google's servers...`); + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); + if (uploadToken) { + newMediaItems.push({ + description: element.description, + simpleMediaItem: { uploadToken } + }); + } else { + console.log("FAIL!", element.url, element.description); + failed++; + } + })); + }, + interval: 3000 + } }); - return !uploadToken ? undefined : { - description: element.description, - simpleMediaItem: { uploadToken } - }; - })); - if (!newMediaItems.every(item => item)) { + } catch (e) { + console.log("WHAT HAPPENED?"); + console.log(e); + } + if (failed) { return _error(res, tokenError); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( -- cgit v1.2.3-70-g09d2 From dcbbfe6d34e89df49069a0ede64df0dc5adc6056 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 13 Sep 2019 20:17:19 -0400 Subject: fixed batching and refactor --- src/client/northstar/utils/Extensions.ts | 13 ++--- .../util/Import & Export/DirectoryImportBox.tsx | 33 ++++++------ src/client/util/UtilExtensions.ts | 62 +++++++++++----------- src/server/apis/google/GooglePhotosUploadUtils.ts | 51 ++++++++---------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 50 +++++++---------- 6 files changed, 95 insertions(+), 116 deletions(-) (limited to 'src/server') diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts index 720f4a062..c866d1bc3 100644 --- a/src/client/northstar/utils/Extensions.ts +++ b/src/client/northstar/utils/Extensions.ts @@ -20,22 +20,17 @@ String.prototype.Truncate = function (length: number, replacement: string): Stri return target; }; -interface Action { - handler: (batch: T[]) => any; - interval?: number; -} -interface BatchParameters { - size: number; - action?: Action; -} +type BatchHandler = (batch: I[]) => O[] | Promise; interface Array { - batch(parameters: BatchParameters): Promise; + batch(batchSize: number): T[][]; + batchAction(batchSize: number, handler: BatchHandler, interval?: number): Promise; lastElement(): T; } Array.prototype.batch = extensions.Batch; +Array.prototype.batchAction = extensions.BatchAction; Array.prototype.lastElement = function () { if (!this.length) { diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 5915f3412..1ae0e7525 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -95,25 +95,19 @@ export default class DirectoryImportBox extends React.Component let sizes: number[] = []; let modifiedDates: number[] = []; - const uploads: FileResponse[] = []; + const localUpload = async (batch: File[]) => { + sizes.push(...batch.map(file => file.size)); + modifiedDates.push(...batch.map(file => file.lastModified)); - await validated.batch({ - size: 15, - action: { - handler: async (batch: File[]) => { - sizes.push(...batch.map(file => file.size)); - modifiedDates.push(...batch.map(file => file.lastModified)); + let formData = new FormData(); + batch.forEach(file => formData.append(Utils.GenerateGuid(), file)); + const parameters = { method: 'POST', body: formData }; - let formData = new FormData(); - batch.forEach(file => formData.append(Utils.GenerateGuid(), file)); - const parameters = { method: 'POST', body: formData }; - - uploads.push(...(await (await fetch(Utils.prepend(RouteStore.upload), parameters)).json())); + runInAction(() => this.completed += batch.length); + return (await fetch(Utils.prepend(RouteStore.upload), parameters)).json(); + }; - runInAction(() => this.completed += batch.length); - } - } - }); + const uploads = await validated.batchAction(15, localUpload); await Promise.all(uploads.map(async upload => { const type = upload.type; @@ -149,7 +143,12 @@ export default class DirectoryImportBox extends React.Component }; let parent = this.props.ContainingCollectionView; if (parent) { - let importContainer = Docs.Create.MasonryDocument(docs, options); + let importContainer: Doc; + if (docs.length < 50) { + importContainer = Docs.Create.MasonryDocument(docs, options); + } else { + importContainer = Docs.Create.SchemaDocument([], docs, options); + } await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); importContainer.singleColumn = false; Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); diff --git a/src/client/util/UtilExtensions.ts b/src/client/util/UtilExtensions.ts index 1e277b242..0bf9f4e97 100644 --- a/src/client/util/UtilExtensions.ts +++ b/src/client/util/UtilExtensions.ts @@ -1,39 +1,41 @@ -module.exports.Batch = async function (parameters: BatchParameters) { - const { size, action } = parameters; +module.exports.Batch = function (batchSize: number): T[][] { const batches: T[][] = []; let i = 0; while (i < this.length) { - const cap = Math.min(i + size, this.length); + const cap = Math.min(i + batchSize, this.length); batches.push(this.slice(i, cap)); i = cap; } - console.log(`Beginning action on ${this.length} elements, split into ${batches.length} groups => ${batches.map(batch => batch.length).join(", ")}`); - if (action) { - const { handler, interval } = action; - if (!interval || batches.length === 1) { - for (let batch of batches) { - await handler(batch); - } - } else { - return new Promise(resolve => { - const iterator = batches[Symbol.iterator](); - const quota = batches.length; - let completed = 0; - const tag = setInterval(async () => { - const next = iterator.next(); - if (next.done) { - clearInterval(tag); - return; - } - const batch = next.value; - console.log(`Handling next batch with ${batch.length} elements`); - await handler(batch); - if (++completed === quota) { - resolve(batches); - } - }, interval); - }); + return batches; +}; + +module.exports.BatchAction = async function (batchSize: number, handler: BatchHandler, interval?: number): Promise { + if (!this.length) { + return []; + } + let collector: O[] = []; + const batches = this.batch(batchSize); + if (!interval || batches.length === 1) { + for (let batch of batches) { + collector.push(...(await handler(batch))); } + } else { + return new Promise(resolve => { + const iterator = batches[Symbol.iterator](); + let completed = 0; + const tag = setInterval(async () => { + const next = iterator.next(); + if (next.done) { + clearInterval(tag); + return; + } + const batch = next.value; + collector.push(...(await handler(batch))); + if (++completed === batches.length) { + resolve(collector); + } + }, interval); + }); } - return batches; + return collector; }; \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 3989590c6..e640f2a85 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -40,7 +40,7 @@ export namespace GooglePhotosUploadUtils { }; export const DispatchGooglePhotosUpload = async (url: string) => { - const body = await request(url, { encoding: null }).catch(error => console.log("Error in streaming body!", error)); + const body = await request(url, { encoding: null }); const parameters = { method: 'POST', headers: { @@ -57,37 +57,30 @@ export namespace GooglePhotosUploadUtils { return reject(error); } resolve(body); - }).catch(error => console.log("Error in literal uploading process to Google's servers!", error))).catch(error => console.log("Error in literal uploading process to Google's servers!", error)); + })); }; export const CreateMediaItems = async (newMediaItems: NewMediaItem[], album?: { id: string }): Promise => { - const newMediaItemResults: NewMediaItemResult[] = []; - await newMediaItems.batch({ - size: 50, - action: { - handler: async (batch: NewMediaItem[]) => { - console.log(batch.length); - const parameters = { - method: 'POST', - headers: headers('json'), - uri: prepend('mediaItems:batchCreate'), - body: { newMediaItems: batch } as any, - json: true - }; - album && (parameters.body.albumId = album.id); - newMediaItemResults.push(...(await new Promise((resolve, reject) => { - request(parameters, (error, _response, body) => { - if (error) { - reject(error); - } else { - resolve(body); - } - }); - })).newMediaItemResults); - }, - interval: 1000 - } - }); + const createFromUploadTokens = async (batch: NewMediaItem[]) => { + const parameters = { + method: 'POST', + headers: headers('json'), + uri: prepend('mediaItems:batchCreate'), + body: { newMediaItems: batch } as any, + json: true + }; + album && (parameters.body.albumId = album.id); + return (await new Promise((resolve, reject) => { + request(parameters, (error, _response, body) => { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + })).newMediaItemResults; + }; + const newMediaItemResults = await newMediaItems.batchAction(50, createFromUploadTokens, 1000); return { newMediaItemResults }; }; diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index d8e0eae21..98d735acd 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyDB0wsV3-oS6q5TFuJSmH1YP_SPf_X6RHaJVmfqj0NTCtaPLFonZRxdT52kUkiHJgAoRizxZvlSIGptXKfnmG4BFouhgyo9ZKP0QtOH-kPR9b9x5WhGCd5NWqz0A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568412547334} \ No newline at end of file +{"access_token":"ya29.ImCDB7HE7-5O12GlhloCz2YWbDC5s8drlIs65oaPUVjAgL66RZMmIV8BptOs2X66ZWvQLCbRourz3ubcQooIuyzgpR8D1IVVm577RC5iyA2xB1Y1GNKZbHgpX3g8yGGmbS8","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568423265295} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 542a4ea65..79e9155d2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -582,9 +582,7 @@ app.post( const filename = path.basename(location); await UploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`)); results.push({ name, type, path: `/files/${filename}` }); - console.log(path.basename(name)); } - console.log("All files traversed!"); _success(res, results); }); } @@ -836,44 +834,36 @@ export interface NewMediaItem { } Array.prototype.batch = extensions.Batch; +Array.prototype.batchAction = extensions.BatchAction; app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { const mediaInput: GooglePhotosUploadUtils.MediaInput[] = req.body.media; await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); - const newMediaItems: NewMediaItem[] = []; let failed = 0; - const size = 25; - - try { - await mediaInput.batch({ - size, - action: { - handler: async (batch: GooglePhotosUploadUtils.MediaInput[]) => { - await Promise.all(batch.map(async element => { - console.log(`Uploading ${element.url} to Google's servers...`); - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); - if (uploadToken) { - newMediaItems.push({ - description: element.description, - simpleMediaItem: { uploadToken } - }); - } else { - console.log("FAIL!", element.url, element.description); - failed++; - } - })); - }, - interval: 3000 + + const dispatchUpload = async (batch: GooglePhotosUploadUtils.MediaInput[]) => { + const newMediaItems: NewMediaItem[] = []; + for (let element of batch) { + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); + if (!uploadToken) { + failed++; + } else { + newMediaItems.push({ + description: element.description, + simpleMediaItem: { uploadToken } + }); } - }); - } catch (e) { - console.log("WHAT HAPPENED?"); - console.log(e); - } + } + return newMediaItems; + }; + + const newMediaItems = await mediaInput.batchAction(25, dispatchUpload, 3000); + if (failed) { return _error(res, tokenError); } + GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( result => _success(res, result.newMediaItemResults), error => _error(res, mediaError, error) -- cgit v1.2.3-70-g09d2 From e4d6f6a643ca07516ec3c8eb4a542c69cfb7b1a2 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 13 Sep 2019 22:30:31 -0400 Subject: updates --- src/client/northstar/utils/Extensions.ts | 22 +++++- .../util/Import & Export/DirectoryImportBox.tsx | 4 +- src/client/util/UtilExtensions.ts | 92 +++++++++++++++++----- src/server/apis/google/GooglePhotosUploadUtils.ts | 2 +- src/server/index.ts | 9 ++- 5 files changed, 100 insertions(+), 29 deletions(-) (limited to 'src/server') diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts index c866d1bc3..00c1e113c 100644 --- a/src/client/northstar/utils/Extensions.ts +++ b/src/client/northstar/utils/Extensions.ts @@ -20,17 +20,31 @@ String.prototype.Truncate = function (length: number, replacement: string): Stri return target; }; - -type BatchHandler = (batch: I[]) => O[] | Promise; +type BatchConverterSync = (batch: I[]) => O[]; +type BatchHandlerSync = (batch: I[]) => void; +type BatchConverterAsync = (batch: I[]) => Promise; +type BatchHandlerAsync = (batch: I[]) => Promise; +type BatchConverter = BatchConverterSync | BatchConverterAsync; +type BatchHandler = BatchHandlerSync | BatchHandlerAsync; interface Array { batch(batchSize: number): T[][]; - batchAction(batchSize: number, handler: BatchHandler, interval?: number): Promise; + executeInBatches(batchSize: number, handler: BatchHandlerSync): void; + convertInBatches(batchSize: number, handler: BatchConverterSync): O[]; + executeInBatchesAsync(batchSize: number, handler: BatchHandler): Promise; + convertInBatchesAsync(batchSize: number, handler: BatchConverter): Promise; + executeInBatchesAtInterval(batchSize: number, handler: BatchHandler, interval: number): Promise; + convertInBatchesAtInterval(batchSize: number, handler: BatchConverter, interval: number): Promise; lastElement(): T; } Array.prototype.batch = extensions.Batch; -Array.prototype.batchAction = extensions.BatchAction; +Array.prototype.executeInBatches = extensions.ExecuteInBatches; +Array.prototype.convertInBatches = extensions.ConvertInBatches; +Array.prototype.executeInBatchesAsync = extensions.ExecuteInBatchesAsync; +Array.prototype.convertInBatchesAsync = extensions.ConvertInBatchesAsync; +Array.prototype.executeInBatchesAtInterval = extensions.ExecuteInBatchesAtInterval; +Array.prototype.convertInBatchesAtInterval = extensions.ConvertInBatchesAtInterval; Array.prototype.lastElement = function () { if (!this.length) { diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 1ae0e7525..a625e06c0 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -95,7 +95,7 @@ export default class DirectoryImportBox extends React.Component let sizes: number[] = []; let modifiedDates: number[] = []; - const localUpload = async (batch: File[]) => { + const uploadLocally = async (batch: File[]) => { sizes.push(...batch.map(file => file.size)); modifiedDates.push(...batch.map(file => file.lastModified)); @@ -107,7 +107,7 @@ export default class DirectoryImportBox extends React.Component return (await fetch(Utils.prepend(RouteStore.upload), parameters)).json(); }; - const uploads = await validated.batchAction(15, localUpload); + const uploads = await validated.convertInBatchesAsync(15, uploadLocally); await Promise.all(uploads.map(async upload => { const type = upload.type; diff --git a/src/client/util/UtilExtensions.ts b/src/client/util/UtilExtensions.ts index 0bf9f4e97..3eeec6ca7 100644 --- a/src/client/util/UtilExtensions.ts +++ b/src/client/util/UtilExtensions.ts @@ -9,33 +9,85 @@ module.exports.Batch = function (batchSize: number): T[][] { return batches; }; -module.exports.BatchAction = async function (batchSize: number, handler: BatchHandler, interval?: number): Promise { +module.exports.ExecuteBatches = function (batchSize: number, handler: BatchHandlerSync): void { + if (this.length) { + for (let batch of this.batch(batchSize)) { + handler(batch); + } + } +}; + +module.exports.ConvertInBatches = function (batchSize: number, handler: BatchConverterSync): O[] { if (!this.length) { return []; } let collector: O[] = []; - const batches = this.batch(batchSize); - if (!interval || batches.length === 1) { - for (let batch of batches) { - collector.push(...(await handler(batch))); + for (let batch of this.batch(batchSize)) { + collector.push(...handler(batch)); + } + return collector; +}; + +module.exports.ExecuteInBatchesAsync = async function (batchSize: number, handler: BatchHandler): Promise { + if (this.length) { + for (let batch of this.batch(batchSize)) { + await handler(batch); } - } else { - return new Promise(resolve => { - const iterator = batches[Symbol.iterator](); - let completed = 0; - const tag = setInterval(async () => { - const next = iterator.next(); - if (next.done) { - clearInterval(tag); - return; + } +}; + +module.exports.ConvertInBatchesAsync = async function (batchSize: number, handler: BatchConverter): Promise { + if (!this.length) { + return []; + } + let collector: O[] = []; + for (let batch of this.batch(batchSize)) { + collector.push(...(await handler(batch))); + } + return collector; +}; + +module.exports.ExecuteInBatchesAtInterval = async function (batchSize: number, handler: BatchHandler, interval: number): Promise { + if (!this.length) { + return; + } + const batches = this.batch(batchSize); + return new Promise(resolve => { + const iterator = batches[Symbol.iterator](); + let completed = 0; + const tag = setInterval(async () => { + const next = iterator.next(); + if (next.done) { + clearInterval(tag); + } else { + await handler(next.value); + if (++completed === batches.length) { + resolve(); } - const batch = next.value; - collector.push(...(await handler(batch))); + } + }, interval * 1000); + }); +}; + +module.exports.ConvertInBatchesAtInterval = async function (batchSize: number, handler: BatchConverter, interval: number): Promise { + if (!this.length) { + return []; + } + let collector: O[] = []; + const batches = this.batch(batchSize); + return new Promise(resolve => { + const iterator = batches[Symbol.iterator](); + let completed = 0; + const tag = setInterval(async () => { + const next = iterator.next(); + if (next.done) { + clearInterval(tag); + } else { + collector.push(...(await handler(next.value))); if (++completed === batches.length) { resolve(collector); } - }, interval); - }); - } - return collector; + } + }, interval * 1000); + }); }; \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index e640f2a85..e1478a097 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -80,7 +80,7 @@ export namespace GooglePhotosUploadUtils { }); })).newMediaItemResults; }; - const newMediaItemResults = await newMediaItems.batchAction(50, createFromUploadTokens, 1000); + const newMediaItemResults = await newMediaItems.convertInBatchesAtInterval(50, createFromUploadTokens, 1); return { newMediaItemResults }; }; diff --git a/src/server/index.ts b/src/server/index.ts index 79e9155d2..8767be17d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -834,7 +834,12 @@ export interface NewMediaItem { } Array.prototype.batch = extensions.Batch; -Array.prototype.batchAction = extensions.BatchAction; +Array.prototype.executeInBatches = extensions.ExecuteInBatches; +Array.prototype.convertInBatches = extensions.ConvertInBatches; +Array.prototype.executeInBatchesAsync = extensions.ExecuteInBatchesAsync; +Array.prototype.convertInBatchesAsync = extensions.ConvertInBatchesAsync; +Array.prototype.executeInBatchesAtInterval = extensions.ExecuteInBatchesAtInterval; +Array.prototype.convertInBatchesAtInterval = extensions.ConvertInBatchesAtInterval; app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { const mediaInput: GooglePhotosUploadUtils.MediaInput[] = req.body.media; @@ -858,7 +863,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { return newMediaItems; }; - const newMediaItems = await mediaInput.batchAction(25, dispatchUpload, 3000); + const newMediaItems = await mediaInput.convertInBatchesAtInterval(25, dispatchUpload, 3); if (failed) { return _error(res, tokenError); -- cgit v1.2.3-70-g09d2 From fc81c0f8ab727ad959eb6b416aff9c190e306adb Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 13 Sep 2019 22:42:30 -0400 Subject: almost nothing --- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/server') diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 98d735acd..05d0aa53b 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.ImCDB7HE7-5O12GlhloCz2YWbDC5s8drlIs65oaPUVjAgL66RZMmIV8BptOs2X66ZWvQLCbRourz3ubcQooIuyzgpR8D1IVVm577RC5iyA2xB1Y1GNKZbHgpX3g8yGGmbS8","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568423265295} \ No newline at end of file +{"access_token":"ya29.GlyDB1Mzjd6_AtAPWPSL1oUWGpmrknNNsJGki6iXlMOJGRbWPQ08dOD3tC2DYlSJN2JgTsMlfAIpjZZ9to3conAdAubgnKfLi7XiHCe5QPcw_G65oSS4E5g9XyuloA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568432027550} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 8767be17d..eea467eec 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -863,7 +863,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { return newMediaItems; }; - const newMediaItems = await mediaInput.convertInBatchesAtInterval(25, dispatchUpload, 3); + const newMediaItems = await mediaInput.convertInBatchesAtInterval(25, dispatchUpload, 3); if (failed) { return _error(res, tokenError); -- cgit v1.2.3-70-g09d2 From 57d6b3da9a918e90d6472c11bac01166e4020185 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 14 Sep 2019 03:05:37 -0400 Subject: fixed batching --- src/client/util/UtilExtensions.ts | 42 +++++++++++++++------------ src/server/credentials/google_docs_token.json | 2 +- 2 files changed, 24 insertions(+), 20 deletions(-) (limited to 'src/server') diff --git a/src/client/util/UtilExtensions.ts b/src/client/util/UtilExtensions.ts index 3eeec6ca7..eca10c3b1 100644 --- a/src/client/util/UtilExtensions.ts +++ b/src/client/util/UtilExtensions.ts @@ -52,20 +52,22 @@ module.exports.ExecuteInBatchesAtInterval = async function (batchSize: number return; } const batches = this.batch(batchSize); - return new Promise(resolve => { + return new Promise(async resolve => { const iterator = batches[Symbol.iterator](); let completed = 0; - const tag = setInterval(async () => { + while (true) { const next = iterator.next(); - if (next.done) { - clearInterval(tag); - } else { - await handler(next.value); - if (++completed === batches.length) { + await new Promise(resolve => { + setTimeout(async () => { + await handler(next.value); resolve(); - } + }, interval * 1000); + }); + if (++completed === batches.length) { + break; } - }, interval * 1000); + } + resolve(); }); }; @@ -75,19 +77,21 @@ module.exports.ConvertInBatchesAtInterval = async function (batchSize: num } let collector: O[] = []; const batches = this.batch(batchSize); - return new Promise(resolve => { + return new Promise(async resolve => { const iterator = batches[Symbol.iterator](); let completed = 0; - const tag = setInterval(async () => { + while (true) { const next = iterator.next(); - if (next.done) { - clearInterval(tag); - } else { - collector.push(...(await handler(next.value))); - if (++completed === batches.length) { - resolve(collector); - } + await new Promise(resolve => { + setTimeout(async () => { + collector.push(...(await handler(next.value))); + resolve(); + }, interval * 1000); + }); + if (++completed === batches.length) { + resolve(collector); + break; } - }, interval * 1000); + } }); }; \ 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 05d0aa53b..bb8e1d817 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyDB1Mzjd6_AtAPWPSL1oUWGpmrknNNsJGki6iXlMOJGRbWPQ08dOD3tC2DYlSJN2JgTsMlfAIpjZZ9to3conAdAubgnKfLi7XiHCe5QPcw_G65oSS4E5g9XyuloA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568432027550} \ No newline at end of file +{"access_token":"ya29.ImCDBxXGjYmUr-TU0k1J9B4MSb6dZEvaDi7jXAZxV3EESxYIHmPbrbCSTHRi-8DXFwKOS-x6NRE5HZ3x5hv6qpWbkPVg0-_wLjBOVdBH4YiIRUgOEKicR_tFL5LxzboL-0M","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568444910512} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 10f77ee4cc03b3cf4356ebb645432c4b53fe8ede Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 14 Sep 2019 03:15:28 -0400 Subject: refresh --- src/server/credentials/google_docs_token.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/server') diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index bb8e1d817..a5197b5f3 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.ImCDBxXGjYmUr-TU0k1J9B4MSb6dZEvaDi7jXAZxV3EESxYIHmPbrbCSTHRi-8DXFwKOS-x6NRE5HZ3x5hv6qpWbkPVg0-_wLjBOVdBH4YiIRUgOEKicR_tFL5LxzboL-0M","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568444910512} \ No newline at end of file +{"access_token":"ya29.ImCDByMCQwL1GEi2BT-yGcoD7Y1Bn-UY-N-Nguu5mijnhOkahApylC6HAJJ8mbkoGdFxywbn-yIkJkr6xQZkZMa2TW29ekjl_791aEvnr_ZUn2O-xJ7r909Rm1FuOhcGdwQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568448563751} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 4f382d9629fbfdf6502782bbb8f39dba06ae51fa Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 14 Sep 2019 03:31:20 -0400 Subject: added header fields and lowered upload threshold --- src/client/util/Import & Export/DirectoryImportBox.tsx | 5 +++-- src/server/index.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src/server') diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index a625e06c0..93ab5cb3b 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -19,6 +19,7 @@ import { List } from "../../../new_fields/List"; import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; const unsupported = ["text/html", "text/plain"]; interface FileResponse { @@ -147,7 +148,8 @@ export default class DirectoryImportBox extends React.Component if (docs.length < 50) { importContainer = Docs.Create.MasonryDocument(docs, options); } else { - importContainer = Docs.Create.SchemaDocument([], docs, options); + const headers = ["title", "size"].map(key => new SchemaHeaderField(key)); + importContainer = Docs.Create.SchemaDocument(headers, docs, options); } await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); importContainer.singleColumn = false; @@ -200,7 +202,6 @@ export default class DirectoryImportBox extends React.Component metadata.splice(index, 1); } } - } } diff --git a/src/server/index.ts b/src/server/index.ts index eea467eec..b18059053 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -863,7 +863,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { return newMediaItems; }; - const newMediaItems = await mediaInput.convertInBatchesAtInterval(25, dispatchUpload, 3); + const newMediaItems = await mediaInput.convertInBatchesAtInterval(25, dispatchUpload, 1); if (failed) { return _error(res, tokenError); -- cgit v1.2.3-70-g09d2 From e2e642dfb3d71ea37c4d521d93ab16f166cc63cf Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 14 Sep 2019 05:06:59 -0400 Subject: update routine --- src/client/northstar/utils/Extensions.ts | 8 +-- .../util/Import & Export/DirectoryImportBox.scss | 6 ++ .../util/Import & Export/DirectoryImportBox.tsx | 70 ++++++++++++++++------ src/client/util/UtilExtensions.ts | 20 +++++-- src/client/views/MainView.tsx | 6 -- src/server/credentials/google_docs_token.json | 2 +- 6 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 src/client/util/Import & Export/DirectoryImportBox.scss (limited to 'src/server') diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts index 00c1e113c..f1fddf6c8 100644 --- a/src/client/northstar/utils/Extensions.ts +++ b/src/client/northstar/utils/Extensions.ts @@ -20,10 +20,10 @@ String.prototype.Truncate = function (length: number, replacement: string): Stri return target; }; -type BatchConverterSync = (batch: I[]) => O[]; -type BatchHandlerSync = (batch: I[]) => void; -type BatchConverterAsync = (batch: I[]) => Promise; -type BatchHandlerAsync = (batch: I[]) => Promise; +type BatchConverterSync = (batch: I[], isFullBatch: boolean) => O[]; +type BatchHandlerSync = (batch: I[], isFullBatch: boolean) => void; +type BatchConverterAsync = (batch: I[], isFullBatch: boolean) => Promise; +type BatchHandlerAsync = (batch: I[], isFullBatch: boolean) => Promise; type BatchConverter = BatchConverterSync | BatchConverterAsync; type BatchHandler = BatchHandlerSync | BatchHandlerAsync; diff --git a/src/client/util/Import & Export/DirectoryImportBox.scss b/src/client/util/Import & Export/DirectoryImportBox.scss new file mode 100644 index 000000000..d33cb524b --- /dev/null +++ b/src/client/util/Import & Export/DirectoryImportBox.scss @@ -0,0 +1,6 @@ +.phase { + position: absolute; + top: 15px; + left: 15px; + font-style: italic; +} \ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 93ab5cb3b..7634d8234 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,9 +1,8 @@ import "fs"; import React = require("react"); -import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; -import { DocServer } from "../../DocServer"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; import { RouteStore } from "../../../server/RouteStore"; -import { action, observable, autorun, runInAction, computed } from "mobx"; +import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx"; import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; import Measure, { ContentRect } from "react-measure"; import { library } from '@fortawesome/fontawesome-svg-core'; @@ -20,6 +19,7 @@ import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import "./DirectoryImportBox.scss"; const unsupported = ["text/html", "text/plain"]; interface FileResponse { @@ -34,6 +34,8 @@ export default class DirectoryImportBox extends React.Component @observable private top = 0; @observable private left = 0; private dimensions = 50; + @observable private phase = ""; + private disposer: Opt; @observable private entries: ImportMetadataEntry[] = []; @@ -73,7 +75,10 @@ export default class DirectoryImportBox extends React.Component } handleSelection = async (e: React.ChangeEvent) => { - runInAction(() => this.uploading = true); + runInAction(() => { + this.uploading = true; + this.phase = "Initializing download..."; + }); let docs: Doc[] = []; @@ -108,8 +113,12 @@ export default class DirectoryImportBox extends React.Component return (await fetch(Utils.prepend(RouteStore.upload), parameters)).json(); }; + runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); + const uploads = await validated.convertInBatchesAsync(15, uploadLocally); + runInAction(() => this.phase = `Creating documents from uploads...`); + await Promise.all(uploads.map(async upload => { const type = upload.type; const path = Utils.prepend(upload.path); @@ -151,8 +160,10 @@ export default class DirectoryImportBox extends React.Component const headers = ["title", "size"].map(key => new SchemaHeaderField(key)); importContainer = Docs.Create.SchemaDocument(headers, docs, options); } - await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); + runInAction(() => this.phase = 'External: uploading files to Google Photos...'); importContainer.singleColumn = false; + await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); + runInAction(() => this.phase = 'All files uploaded to Google Photos...'); Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); DocumentManager.Instance.jumpToDocument(importContainer, true); @@ -168,6 +179,14 @@ export default class DirectoryImportBox extends React.Component componentDidMount() { this.selector.current!.setAttribute("directory", ""); this.selector.current!.setAttribute("webkitdirectory", ""); + this.disposer = reaction( + () => this.completed, + completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`) + ); + } + + componentWillUnmount() { + this.disposer && this.disposer(); } @action @@ -218,10 +237,38 @@ export default class DirectoryImportBox extends React.Component percent = percent.split(".")[0]; percent = percent.startsWith("100") ? "99" : percent; let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; + const message = {this.phase}; + const centerPiece = this.phase.includes("Google Photos") ? + + :
    {percent}%
    ; return ( {({ measureRef }) =>
    + {message} opacity: showRemoveLabel ? 1 : 0, transition: "0.4s opacity ease" }}>Template will be {persistent ? "kept" : "removed"} after upload

    -
    {percent}%
    + {centerPiece}
    (batchSize: number): T[][] { module.exports.ExecuteBatches = function (batchSize: number, handler: BatchHandlerSync): void { if (this.length) { for (let batch of this.batch(batchSize)) { - handler(batch); + const isFullBatch = batch.length === batchSize; + handler(batch, isFullBatch); } } }; @@ -23,7 +24,8 @@ module.exports.ConvertInBatches = function (batchSize: number, handler: Ba } let collector: O[] = []; for (let batch of this.batch(batchSize)) { - collector.push(...handler(batch)); + const isFullBatch = batch.length === batchSize; + collector.push(...handler(batch, isFullBatch)); } return collector; }; @@ -31,7 +33,8 @@ module.exports.ConvertInBatches = function (batchSize: number, handler: Ba module.exports.ExecuteInBatchesAsync = async function (batchSize: number, handler: BatchHandler): Promise { if (this.length) { for (let batch of this.batch(batchSize)) { - await handler(batch); + const isFullBatch = batch.length === batchSize; + await handler(batch, isFullBatch); } } }; @@ -42,7 +45,8 @@ module.exports.ConvertInBatchesAsync = async function (batchSize: number, } let collector: O[] = []; for (let batch of this.batch(batchSize)) { - collector.push(...(await handler(batch))); + const isFullBatch = batch.length === batchSize; + collector.push(...(await handler(batch, isFullBatch))); } return collector; }; @@ -59,7 +63,9 @@ module.exports.ExecuteInBatchesAtInterval = async function (batchSize: number const next = iterator.next(); await new Promise(resolve => { setTimeout(async () => { - await handler(next.value); + const batch = next.value; + const isFullBatch = batch.length === batchSize; + await handler(batch, isFullBatch); resolve(); }, interval * 1000); }); @@ -84,7 +90,9 @@ module.exports.ConvertInBatchesAtInterval = async function (batchSize: num const next = iterator.next(); await new Promise(resolve => { setTimeout(async () => { - collector.push(...(await handler(next.value))); + const batch = next.value; + const isFullBatch = batch.length === batchSize; + collector.push(...(await handler(batch, isFullBatch))); resolve(); }, interval * 1000); }); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index f7b66cae3..d1e0733a7 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -576,12 +576,6 @@ export class MainView extends React.Component {
    )}
  • - {ClientUtils.RELEASE ? [] : [ -
  • , -
  • , -
  • , -
  • - ]}
  • ; diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index 8f06cf770..8ef9f3be6 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -98,7 +98,7 @@ export class ScriptBox extends React.Component { // tslint:disable-next-line: no-unnecessary-callback-wrapper let params: string[] = []; let setParams = (p: string[]) => params.splice(0, params.length, ...p); - let scriptingBox = overlayDisposer()} onSave={(text, onError) => { + let scriptingBox = { if (prewrapper) { text = prewrapper + text + (postwrapper ? postwrapper : ""); } diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index e05195ca0..1eb380e0b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -135,19 +135,17 @@ export class ScriptingRepl extends React.Component { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = script.run({ args: this.args }); - if (!result.success) { - this.commands.push({ command: this.commandString, result: result.error.toString() }); - return; - } - this.commands.push({ command: this.commandString, result: result.result }); - this.commandsHistory.push(this.commandString); + const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })); + if (result.success) { + this.commands.push({ command: this.commandString, result: result.result }); + this.commandsHistory.push(this.commandString); - this.maybeScrollToBottom(); + this.maybeScrollToBottom(); - this.commandString = ""; - this.commandBuffer = ""; - this.historyIndex = -1; + this.commandString = ""; + this.commandBuffer = ""; + this.historyIndex = -1; + } break; } case "ArrowUp": { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 4e371ffd1..9e5e62e03 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,6 +1,5 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { DocumentType } from "../documents/DocumentTypes"; import { DocumentManager } from "../util/DocumentManager"; import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; @@ -9,6 +8,7 @@ import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import { Template, Templates } from "./Templates"; import React = require("react"); +import { Doc } from "../../new_fields/Doc"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -29,12 +29,12 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool } } @observer -class ChromeToggle extends React.Component<{ checked: boolean, toggle: (event: React.ChangeEvent) => void }> { +class OtherToggle extends React.Component<{ checked: boolean, name: string, toggle: (event: React.ChangeEvent) => void }> { render() { return (
  • this.props.toggle(event)} /> - Chrome + {this.props.name}
  • ); } @@ -50,33 +50,23 @@ export class TemplateMenu extends React.Component { @observable private _hidden: boolean = true; dragRef = React.createRef(); - constructor(props: TemplateMenuProps) { - super(props); + toggleCustom = (e: React.ChangeEvent): void => { + this.props.docs.map(dv => dv.setCustomView(e.target.checked)); } - toggleCustom = (e: React.MouseEvent): void => { - this.props.docs.map(dv => { - if (dv.Document.type !== DocumentType.COL && dv.Document.type !== DocumentType.TEMPLATE) { - DocumentView.makeCustomViewClicked(dv.props.Document); - } else if (dv.Document.nativeLayout) { - DocumentView.makeNativeViewClicked(dv.props.Document); - } - }); - } - - toggleFloat = (e: React.MouseEvent): void => { + toggleFloat = (e: React.ChangeEvent): void => { SelectionManager.DeselectAll(); let topDocView = this.props.docs[0]; let topDoc = topDocView.props.Document; let xf = topDocView.props.ScreenToLocalTransform(); - let ex = e.clientX; - let ey = e.clientY; + let ex = e.target.clientLeft; + let ey = e.target.clientTop; undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))(); - if (!topDoc.z) { + if (e.target.checked) { setTimeout(() => { let newDocView = DocumentManager.Instance.getDocumentView(topDoc); if (newDocView) { - let de = new DragManager.DocumentDragData([topDoc], [undefined]); + let de = new DragManager.DocumentDragData([topDoc]); de.moveDocument = topDocView.props.moveDocument; let xf = newDocView.ContentDiv!.getBoundingClientRect(); DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, { @@ -99,16 +89,23 @@ export class TemplateMenu extends React.Component { @action toggleTemplate = (event: React.ChangeEvent, template: Template): void => { if (event.target.checked) { - this.props.docs.map(d => d.props.Document["show" + template.Name] = template.Name.toLowerCase()); + this.props.docs.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase()); } else { - this.props.docs.map(d => d.props.Document["show" + template.Name] = undefined); + this.props.docs.map(d => d.Document["show" + template.Name] = ""); } } @undoBatch @action clearTemplates = (event: React.MouseEvent) => { - Templates.TemplateList.map(template => this.props.docs.map(d => d.props.Document["show" + template.Name] = false)); + Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined)); + ["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => { + if (d.Document.isTemplate && d.props.DataDoc) { + d.Document[field] = undefined; + } else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) { + d.Document[field] = Doc.GetProto(d.Document)[field] = undefined; + } + })); } @action @@ -119,22 +116,26 @@ export class TemplateMenu extends React.Component { @undoBatch @action toggleChrome = (): void => { - this.props.docs.map(dv => dv.layoutDoc.chromeStatus = (dv.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled")); + this.props.docs.map(dv => { + let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document; + layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled"); + }); } render() { + let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout : this.props.docs[0].Document; let templateMenu: Array = []; this.props.templates.forEach((checked, template) => templateMenu.push()); - templateMenu.push(); + templateMenu.push(); + templateMenu.push(); + templateMenu.push(); return (
    this.toggleTemplateActivity()}>+
      {templateMenu} - - - {/* */} + {}
    ); diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 93eaab453..818a41009 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -12,7 +12,6 @@ import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import './CollectionBaseView.scss'; import { DateField } from '../../../new_fields/DateField'; -import { DocumentType } from '../../documents/DocumentTypes'; import { ImageField } from '../../../new_fields/URLField'; export enum CollectionViewType { @@ -81,12 +80,12 @@ export class CollectionBaseView extends React.Component { } } - @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; } active = (): boolean => { var isSelected = this.props.isSelected(); - return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary); + return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; } //TODO should this be observable? @@ -96,7 +95,7 @@ export class CollectionBaseView extends React.Component { this.props.whenActiveChanged(isActive); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { @@ -105,7 +104,6 @@ export class CollectionBaseView extends React.Component { if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer Doc.GetProto(doc).annotationOn = this.props.Document; } - allowDuplicates = true; let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; const value = Cast(targetDataDoc[targetField], listSpec(Doc)); @@ -128,7 +126,8 @@ export class CollectionBaseView extends React.Component { let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); - let index = value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); + let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); + index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)); @@ -142,17 +141,16 @@ export class CollectionBaseView extends React.Component { return false; } + // this is called with the document that was dragged and the collection to move it into. + // if the target collection is the same as this collection, then the move will be allowed. + // otherwise, the document being moved must be able to be removed from its container before + // moving it into the target. @action.bound moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - let self = this; - let targetDataDoc = this.props.Document; - if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) { + if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } - if (this.removeDocument(doc)) { - return addDocument(doc); - } - return false; + return this.removeDocument(doc) ? addDocument(doc) : false; } showIsTagged = () => { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index f184a3944..ef2681410 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,36 +1,35 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faFile } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, Lambda, observable, reaction, trace, computed, runInAction } from "mobx"; +import { action, Lambda, observable, reaction, computed, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; +import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List'; import { FieldId } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { emptyFunction, returnTrue, Utils, returnOne, returnEmptyString } from "../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; +import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { undoBatch } from "../../util/UndoManager"; +import { MainView } from '../MainView'; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; -import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); -import { MainView } from '../MainView'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { Docs } from '../../documents/Documents'; -import { DateField } from '../../../new_fields/DateField'; -import { List } from '../../../new_fields/List'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { ButtonSelector } from './ParentDocumentSelector'; library.add(faFile); @observer @@ -44,7 +43,7 @@ export class CollectionDockingView extends React.Component CollectionDockingView.Instance = this); - } + !CollectionDockingView.Instance && (CollectionDockingView.Instance = this); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; } hack: boolean = false; undohack: any = null; - public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) { + public StartOtherDrag(e: any, dragDocs: Doc[]) { let config: any; if (dragDocs.length === 1) { - config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]); + config = CollectionDockingView.makeDocumentConfig(dragDocs[0], undefined); } else { config = { type: 'row', content: dragDocs.map((doc, i) => { - CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]); + CollectionDockingView.makeDocumentConfig(doc, undefined); }) }; } @@ -90,18 +87,13 @@ export class CollectionDockingView extends React.Component - // this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener. - // onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } + @undoBatch @action public OpenFullScreen(docView: DocumentView) { let document = Doc.MakeAlias(docView.props.Document); - let dataDoc = docView.dataDoc; + let dataDoc = docView.props.DataDoc; let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] @@ -113,6 +105,7 @@ export class CollectionDockingView extends React.Component { @@ -131,21 +124,25 @@ export class CollectionDockingView extends React.Component { + public static CloseRightSplit(document: Doc): boolean { + if (!CollectionDockingView.Instance) return false; + let instance = CollectionDockingView.Instance; let retVal = false; - if (this._goldenLayout.root.contentItems[0].isRow) { - retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { + if (instance._goldenLayout.root.contentItems[0].isRow) { + retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { child.contentItems[0].remove(); - this.layoutChanged(document); + instance.layoutChanged(document); return true; } else { Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { - if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) && + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { child.contentItems[j].remove(); child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); - let docs = Cast(this.props.Document.data, listSpec(Doc)); + let docs = Cast(instance.props.Document.data, listSpec(Doc)); docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); return true; } @@ -156,7 +153,7 @@ export class CollectionDockingView extends React.Component { - let docs = Cast(this.props.Document.data, listSpec(Doc)); + public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) { + if (!CollectionDockingView.Instance) return false; + let instance = CollectionDockingView.Instance; + let docs = Cast(instance.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } @@ -192,15 +192,15 @@ export class CollectionDockingView extends React.Component { Doc.GetProto(document).lastOpened = new DateField; @@ -245,6 +246,7 @@ export class CollectionDockingView extends React.Component { e.preventDefault(); e.stopPropagation(); - DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, { + DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, { handlers: { dragComplete: emptyFunction }, hideSource: false }); }}>, dragSpan); - ReactDOM.render( CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv); - tab.reactComponents = [dragSpan, upDiv]; + ReactDOM.render(, gearSpan); + // ReactDOM.render( { + // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); + // return true; + // }} />, upDiv); + tab.reactComponents = [dragSpan, gearSpan, upDiv]; tab.element.append(dragSpan); + tab.element.append(gearSpan); tab.element.append(upDiv); tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; @@ -538,11 +549,7 @@ export class DockedFrameRenderer extends React.Component { @observable private _isActive: boolean = false; get _stack(): any { - let parent = (this.props as any).glContainer.parent.parent; - if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) { - return parent.parent.contentItems[1]; - } - return parent; + return (this.props as any).glContainer.parent.parent; } constructor(props: any) { super(props); @@ -616,17 +623,18 @@ export class DockedFrameRenderer extends React.Component { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; } + get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; } - addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { + addDocTab = (doc: Doc, dataDoc: Opt, location: string) => { if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); + return true; } else if (location === "onRight") { - CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); + return CollectionDockingView.AddRightSplit(doc, dataDoc); } else if (location === "close") { - CollectionDockingView.Instance.CloseRightSplit(doc); + return CollectionDockingView.CloseRightSplit(doc); } else { - CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); + return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } } @computed get docView() { @@ -640,6 +648,7 @@ export class DockedFrameRenderer extends React.Component { bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} + ruleProvider={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} @@ -652,6 +661,7 @@ export class DockedFrameRenderer extends React.Component { addDocTab={this.addDocTab} pinToPres={this.PinDoc} ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} zoomToScale={emptyFunction} getScale={returnOne} />; } diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index c59107b53..4dac27e60 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -39,7 +39,7 @@ export interface CellProps { Document: Doc; fieldKey: string; renderDepth: number; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; isFocused: boolean; @@ -149,7 +149,9 @@ export class CollectionSchemaCell extends React.Component { DataDoc: this.props.rowProps.original, fieldKey: this.props.rowProps.column.id as string, fieldExt: "", + ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, + ContainingCollectionDoc: this.props.CollectionView.props.Document, isSelected: returnFalse, select: emptyFunction, renderDepth: this.props.renderDepth + 1, @@ -171,7 +173,8 @@ export class CollectionSchemaCell extends React.Component { let onItemDown = (e: React.PointerEvent) => { if (fieldIsDoc) { SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); + this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, + this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); } }; let onPointerEnter = (e: React.PointerEvent): void => { @@ -235,13 +238,11 @@ export class CollectionSchemaCell extends React.Component { return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); }} OnFillDown={async (value: string) => { - let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (!script.compiled) { - return; + const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + if (script.compiled) { + DocListCast(this.props.Document[this.props.fieldKey]). + forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run)); } - const run = script.run; - const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); - val && val.forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, run)); }} />
    diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index ec40043cc..39abc41ec 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -201,12 +201,8 @@ export class MovableRow extends React.Component { @action move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { let targetView = DocumentManager.Instance.getDocumentView(target); - if (targetView) { - let targetContainingColl = targetView.props.ContainingCollectionView; //.props.ContainingCollectionView.props.Document; - if (targetContainingColl) { - let targetContCollDoc = targetContainingColl.props.Document; - return doc !== target && doc !== targetContCollDoc && this.props.removeDoc(doc) && addDoc(doc); - } + if (targetView && targetView.props.ContainingCollectionDoc) { + return doc !== target && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); } return doc !== target && this.props.removeDoc(doc) && addDoc(doc); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 9d83aa6c1..7bd2a1971 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -14,7 +14,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { Gateway } from "../../northstar/manager/Gateway"; -import { SetupDrag, DragManager } from "../../util/DragManager"; +import { DragManager } from "../../util/DragManager"; import { CompileScript, ts, Transformer } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; @@ -32,6 +32,7 @@ import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, Collection import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { DocumentType } from "../../documents/DocumentTypes"; library.add(faCog, faPlus, faSortUp, faSortDown); @@ -161,6 +162,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} childDocs={this.childDocs} renderDepth={this.props.renderDepth} + ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} width={this.previewWidth} height={this.previewHeight} getTransform={this.getPreviewTransform} @@ -194,6 +196,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { childDocs={this.childDocs} CollectionView={this.props.CollectionView} ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} fieldKey={this.props.fieldKey} renderDepth={this.props.renderDepth} moveDocument={this.props.moveDocument} @@ -245,6 +248,7 @@ export interface SchemaTableProps { childDocs?: Doc[]; CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; ContainingCollectionView: Opt; + ContainingCollectionDoc: Opt; fieldKey: string; renderDepth: number; deleteDocument: (document: Doc) => boolean; @@ -252,7 +256,7 @@ export interface SchemaTableProps { ScreenToLocalTransform: () => Transform; active: () => boolean; onDrop: (e: React.DragEvent, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; isSelected: () => boolean; isFocused: (document: Doc) => boolean; @@ -901,6 +905,7 @@ interface CollectionSchemaPreviewProps { fitToBox?: boolean; width: () => number; height: () => number; + ruleProvider: Doc | undefined; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; onClick?: ScriptField; @@ -910,7 +915,7 @@ interface CollectionSchemaPreviewProps { removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; setPreviewScript: (script: string) => void; previewScript?: string; @@ -943,13 +948,12 @@ export class CollectionSchemaPreview extends React.Component { if (de.data instanceof DragManager.DocumentDragData) { - let docDrag = de.data; - let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } }); this.props.childDocs && this.props.childDocs.map(otherdoc => { - let doc = docDrag.draggedDocuments[0]; let target = Doc.GetProto(otherdoc); - target.layout = target.detailedLayout = Doc.MakeDelegate(doc); - computed.compiled && (target.miniLayout = new ComputedField(computed)); + let layoutNative = Doc.MakeTitled("layoutNative"); + layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]"); + target.layoutNative = layoutNative; + target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]); }); e.stopPropagation(); } @@ -995,12 +999,14 @@ export class CollectionSchemaPreview extends React.Component doc) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef(); _heightDisposer?: IReactionDisposer; - _childLayoutDisposer?: IReactionDisposer; _sectionFilterDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @@ -87,10 +86,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => args[1] instanceof Doc && - this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined))); - // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). this._heightDisposer = reaction(() => { if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) { @@ -115,7 +110,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { ); } componentWillUnmount() { - this._childLayoutDisposer && this._childLayoutDisposer(); this._heightDisposer && this._heightDisposer(); this._sectionFilterDisposer && this._sectionFilterDisposer(); } @@ -134,7 +128,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } - @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : ScriptCast(this.Document.onChildClick); } + @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { let height = () => this.getDocHeight(layoutDoc); @@ -144,6 +138,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { DataDocument={dataDoc} showOverlays={this.overlays} renderDepth={this.props.renderDepth} + ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} fitToBox={this.props.fitToBox} onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler} width={width} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index bc4fe7dd7..b3b7b40dd 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -155,6 +155,9 @@ export class CollectionStackingViewFieldColumn extends React.Component NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + let heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; + newDoc.heading = heading; return this.props.parent.props.addDocument(newDoc); } @@ -175,13 +178,9 @@ export class CollectionStackingViewFieldColumn extends React.Component(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; + private _childLayoutDisposer?: IReactionDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer && this.dropDisposer(); if (ele) { @@ -51,33 +54,35 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { this.createDropTarget(ele); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + componentDidMount() { + this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], + async (args) => args[1] instanceof Doc && + this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc)))); + } + componentWillUnmount() { + this._childLayoutDisposer && this._childLayoutDisposer(); + } - get childDocs() { - let self = this; - //TODO tfs: This might not be what we want? - //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]); - let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - if (viewSpecScript) { - let script = viewSpecScript.script; - docs = docs.filter(d => { - let res = script.run({ doc: d }); - if (res.success) { - return res.result; - } - else { - console.log(res.error); - } - }); - } - return docs; + // The data field for rendeing this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. + // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through + // to its children which may be templates. + // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise. + @computed get dataField() { + return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey]; + } + + + get childLayoutPairs() { + return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! })); } get childDocList() { - //TODO tfs: This might not be what we want? - //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - return Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc)); + return Cast(this.dataField, listSpec(Doc)); + } + get childDocs() { + let docs = DocListCast(this.dataField); + const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); + return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } @action @@ -118,7 +123,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { if (de.mods === "AltKey" && de.data.draggedDocuments.length) { this.childDocs.map(doc => - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined) + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc) ); e.stopPropagation(); return true; @@ -127,9 +132,11 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { if (de.data.dropAction || de.data.userDropAction) { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } else if (de.data.moveDocument) { - let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments; - added = movedDocs.reduce((added: boolean, d) => - de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false); + let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments; + // note that it's possible the drag function might create a drop document that's not the same as the + // original dragged document. So we explicitly call addDocument() with a droppedDocument and + added = movedDocs.reduce((added: boolean, d, i) => + de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false); } else { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } @@ -271,6 +278,10 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { promises.push(prom); } } + if (text) { + this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 })); + return; + } if (promises.length) { Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index f5bb76966..e5313f68c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -37,9 +37,10 @@ export interface TreeViewProps { containingCollection: Doc; renderDepth: number; deleteDoc: (doc: Doc) => boolean; + ruleProvider: Doc | undefined; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; @@ -177,7 +178,7 @@ class TreeView extends React.Component { SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)} OnFillDown={undoBatch((value: string) => { Doc.GetProto(this.dataDoc)[key] = value; - let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined; + let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined; if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; return this.props.addDocument(doc); @@ -200,6 +201,7 @@ class TreeView extends React.Component { ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); } ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); e.preventDefault(); @@ -324,6 +326,7 @@ class TreeView extends React.Component { DataDocument={this.resolvedDataDoc} renderDepth={this.props.renderDepth} showOverlays={this.noOverlays} + ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} fitToBox={this.boundsOfCollectionDocument !== undefined} width={this.docWidth} height={this.docHeight} @@ -344,7 +347,7 @@ class TreeView extends React.Component { @computed get renderBullet() { - return
    this.treeViewOpen = !this.treeViewOpen)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> + return
    { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> {}
    ; } @@ -402,7 +405,7 @@ class TreeView extends React.Component {
    ; } public static GetChildElements( - docList: Doc[], + docs: Doc[], treeViewId: string, containingCollection: Doc, dataDoc: Doc | undefined, @@ -411,7 +414,7 @@ class TreeView extends React.Component { remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean, pinToPres: (document: Doc) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, @@ -422,19 +425,9 @@ class TreeView extends React.Component { preventTreeViewOpen: boolean, renderedIds: string[] ) { - let docs = docList.filter(child => !child.excludeFromLibrary && child.opacity !== 0); - let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); + const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { - let script = viewSpecScript.script; - docs = docs.filter(d => { - let res = script.run({ doc: d }); - if (res.success) { - return res.result; - } - else { - console.log(res.error); - } - }); + docs = docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); } let ascending = Cast(containingCollection.sortAscending, "boolean", null); @@ -491,6 +484,7 @@ class TreeView extends React.Component { dataDoc={pair.data} containingCollection={containingCollection} treeViewId={treeViewId} + ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc} key={child[Id]} indentDocument={indent} renderDepth={renderDepth} @@ -544,7 +538,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document + if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); e.stopPropagation(); @@ -559,8 +553,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); openNotifsCol = () => { - if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) { - CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined); + if (CollectionTreeView.NotifsCol) { + this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight"); } } @@ -610,7 +604,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)} OnFillDown={undoBatch((value: string) => { Doc.GetProto(this.props.Document).title = value; - let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined; + let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined; if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 94b49fb98..1f2dc9b5c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -105,6 +105,12 @@ export class CollectionView extends React.Component { subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" }); subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" }); subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); + subItems.push({ + description: "Stacking (AutoHeight)", event: () => { + this.props.Document.viewType = CollectionViewType.Stacking; + this.props.Document.autoHeight = true; + }, icon: "ellipsis-v" + }); subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); switch (this.props.Document.viewType) { case CollectionViewType.Freeform: { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index c897af17e..7510b86a0 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -215,14 +215,11 @@ export class CollectionViewBaseChrome extends React.Component { - let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false }); - if (compiled.compiled) { - this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled); - } - + this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name }); this._keyRestrictions = []; this.addKeyRestrictions([]); } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index 2dd3e49f2..c186d15f8 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -19,4 +19,13 @@ border-right: 0px; border-left: 0px; } +} +.parentDocumentSelector-button { + pointer-events: all; +} +.buttonSelector { + position: absolute; + display: inline-block; + padding-left: 5px; + padding-right: 5px; } \ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index d8475a467..7f2913214 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -8,8 +8,15 @@ import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentManager } from "../../util/DocumentManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { library } from "@fortawesome/fontawesome-svg-core"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +library.add(faEdit); + +type SelectorProps = { Document: Doc, Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; @observer export class SelectorContextMenu extends React.Component { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -83,7 +90,7 @@ export class ParentDocSelector extends React.Component { ); } return ( -

    ^

    @@ -92,3 +99,38 @@ export class ParentDocSelector extends React.Component { ); } } + +@observer +export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> { + @observable hover = false; + + @action + onMouseLeave = () => { + this.hover = false; + } + + @action + onMouseEnter = () => { + this.hover = true; + } + + render() { + let flyout; + if (this.hover) { + let view = DocumentManager.Instance.getDocumentView(this.props.Document); + flyout = !view ? (null) : ( +
    + +
    + ); + } + return ( + + {this.hover ? (null) : } + {flyout} + + ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 2a64a7afb..cfd18ad35 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -1,8 +1,9 @@ .collectionfreeformlinkview-linkLine { stroke: black; transform: translate(10000px,10000px); - // opacity: 0.5; + opacity: 0.8; pointer-events: all; + stroke-width: 3px; } .collectionfreeformlinkview-linkCircle { stroke: rgb(0,0,0); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 12771d11e..df089eb00 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -50,7 +50,6 @@ export class CollectionFreeFormLinkView extends React.Component {/* child[Id] === collid).map(view => DocumentManager.Instance.getDocumentViews(view).map(view => equalViews.push(view))); } - return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document); + return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document); } @computed diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f7bda0a26..eb738d783 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -8,10 +8,10 @@ import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../../new_fields/Types"; +import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue, DateCast } from "../../../../new_fields/Types"; import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; -import { Docs } from "../../../documents/Documents"; +import { Docs, DocumentOptions } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; @@ -24,9 +24,9 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingCanvas } from "../../InkingCanvas"; -import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; -import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; +import { DocumentViewProps, documentSchema } from "../../nodes/DocumentView"; import { pageSchema } from "../../nodes/ImageBox"; import { OverlayElementOptions, OverlayView } from "../../OverlayView"; import PDFMenu from "../../pdf/PDFMenu"; @@ -51,6 +51,9 @@ export const panZoomSchema = createSchema({ scale: "number", arrangeScript: ScriptField, arrangeInit: ScriptField, + useClusters: "boolean", + isRuleProvider: "boolean", + fitToBox: "boolean" }); export interface ViewDefBounds { @@ -161,6 +164,7 @@ export namespace PivotView { y={pos.y} width={pos.width} height={pos.height} + transition={"transform 1s"} jitterRotation={NumCast(target.props.Document.jitterRotation)} {...target.getChildDocumentViewProps(doc)} />, @@ -181,8 +185,8 @@ export namespace PivotView { } -type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>; -const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema); +type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>; +const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema); @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @@ -190,35 +194,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _lastY: number = 0; private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } - private inkKey = "ink"; - private _childLayoutDisposer?: IReactionDisposer; - private _childDisposer?: IReactionDisposer; - - componentDidMount() { - this._childDisposer = reaction(() => this.childDocs, - async (childDocs) => { - let childLayout = Cast(this.props.Document.childLayout, Doc) as Doc; - childLayout && childDocs.map(async doc => { - if (!Doc.AreProtosEqual(childLayout, (await doc).layout as Doc)) { - Doc.ApplyTemplateTo(childLayout, doc, undefined); - } - }); - }); - this._childLayoutDisposer = reaction(() => Cast(this.props.Document.childLayout, Doc), - async (childLayout) => { - this.childDocs.map(async doc => { - if (!Doc.AreProtosEqual(childLayout as Doc, (await doc).layout as Doc)) { - Doc.ApplyTemplateTo(childLayout as Doc, doc, undefined); - } - }); - }); - } - componentWillUnmount() { - this._childDisposer && this._childDisposer(); - this._childLayoutDisposer && this._childLayoutDisposer(); - } - - get parentScaling() { + private get parentScaling() { return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1; } @@ -249,7 +225,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return res; } - @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; } + @computed get fitToBox() { return this.props.fitToBox || this.Document.fitToBox; } @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; } public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') @@ -265,29 +241,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - newBox.heading = 1; - for (let child of this.childDocs) { - if (child.heading === 1) { - newBox.heading = 2; - } + let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0; + if (heading === 0) { + let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 : + DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0); + heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading); } - PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => { - if (!ruleProvider) ruleProvider = this.props.Document; - // saturation shift - // let col = NumCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]); - // let back = Utils.fromRGBAstr(StrCast(this.props.Document.backgroundColor)); - // let hsl = Utils.RGBToHSL(back.r, back.g, back.b); - // let newcol = { h: hsl.h, s: hsl.s + col, l: hsl.l }; - // col && (Doc.GetProto(newBox).backgroundColor = Utils.toRGBAstr(Utils.HSLtoRGB(newcol.h, newcol.s, newcol.l))); - // OR transparency set - let col = StrCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]); - (newBox.backgroundColor === newBox.defaultBackgroundColor) && col && (Doc.GetProto(newBox).backgroundColor = col); - - let round = StrCast(ruleProvider["ruleRounding_" + NumCast(newBox.heading)]); - round && (Doc.GetProto(newBox).borderRounding = round); - newBox.ruleProvider = ruleProvider; - this.addDocument(newBox, false); - }); + !this.Document.isRuleProvider && (newBox.heading = heading); + this.addDocument(newBox, false); } private addDocument = (newBox: Doc, allowDuplicates: boolean) => { this.props.addDocument(newBox, false); @@ -302,14 +264,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } public getActiveDocuments = () => { const curPage = FieldValue(this.Document.curPage, -1); - return this.childDocs.filter(doc => { - var page = NumCast(doc.page, -1); + return this.childLayoutPairs.filter(pair => { + var page = NumCast(pair.layout.page, -1); return page === curPage || page === -1; - }); + }).map(pair => pair.layout); } @computed get fieldExtensionDoc() { - return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); + return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); } intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -342,8 +304,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (de.data instanceof DragManager.DocumentDragData) { if (de.data.droppedDocuments.length) { let z = NumCast(de.data.droppedDocuments[0].z); - let x = (z ? xpo : xp) - de.data.xOffset; - let y = (z ? ypo : yp) - de.data.yOffset; + let x = (z ? xpo : xp) - de.data.offset[0]; + let y = (z ? ypo : yp) - de.data.offset[1]; let dropX = NumCast(de.data.droppedDocuments[0].x); let dropY = NumCast(de.data.droppedDocuments[0].y); de.data.droppedDocuments.forEach(d => { @@ -366,8 +328,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { else if (de.data instanceof DragManager.AnnotationDragData) { if (de.data.dropDocument) { let dragDoc = de.data.dropDocument; - let x = xp - de.data.xOffset; - let y = yp - de.data.yOffset; + let x = xp - de.data.offset[0]; + let y = yp - de.data.offset[1]; let dropX = NumCast(de.data.dropDocument.x); let dropY = NumCast(de.data.dropDocument.y); dragDoc.x = x + NumCast(dragDoc.x) - dropX; @@ -383,7 +345,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { tryDragCluster(e: PointerEvent) { let probe = this.getTransform().transformPoint(e.clientX, e.clientY); - let cluster = this.childDocs.reduce((cluster, cd) => { + let cluster = this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { let cx = NumCast(cd.x) - this._clusterDistance; let cy = NumCast(cd.y) - this._clusterDistance; let cw = NumCast(cd.width) + 2 * this._clusterDistance; @@ -394,7 +356,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return cluster; }, -1); if (cluster !== -1) { - let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster); + let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view let prevSelected = SelectionManager.SelectedDocuments(); @@ -403,13 +365,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { SelectionManager.DeselectAll(); prevSelected.map(dv => SelectionManager.SelectDoc(dv, true)); - let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined)); + let de = new DragManager.DocumentDragData(eles); de.moveDocument = this.props.moveDocument; const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); - const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top); + de.offset = this.getTransform().transformDirection(e.x - left, e.y - top); de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; - de.xOffset = xoff; - de.yOffset = yoff; DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, { handlers: { dragComplete: action(emptyFunction) }, hideSource: !de.dropAction @@ -423,9 +383,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @undoBatch @action - updateClusters() { + updateClusters(useClusters: boolean) { + this.Document.useClusters = useClusters; this.sets.length = 0; - this.childDocs.map(c => { + this.childLayoutPairs.map(pair => pair.layout).map(c => { let included = []; for (let i = 0; i < this.sets.length; i++) { for (let member of this.sets[i]) { @@ -453,20 +414,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @undoBatch @action updateCluster(doc: Doc) { + let childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document.useClusters) { this.sets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); let preferredInd = NumCast(doc.cluster); doc.cluster = -1; this.sets.map((set, i) => set.map(member => { - if (doc.cluster === -1 && Doc.IndexOf(member, this.childDocs) !== -1 && this.boundsOverlap(doc, member)) { + if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && this.boundsOverlap(doc, member)) { doc.cluster = i; } })); - if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, this.childDocs) !== -1).length)) { + if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } this.sets.map((set, i) => { - if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, this.childDocs) !== -1).length) { + if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { doc.cluster = i; } }); @@ -481,22 +443,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } getClusterColor = (doc: Doc) => { - if (this.props.Document.useClusters) { - let cluster = NumCast(doc.cluster); + let clusterColor = ""; + let cluster = NumCast(doc.cluster); + if (this.Document.useClusters) { if (this.sets.length <= cluster) { - setTimeout(() => this.updateCluster(doc), 0);// this.updateClusters(), 0); - return ""; + setTimeout(() => this.updateCluster(doc), 0); + } else { + // choose a cluster color from a palette + let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; + clusterColor = colors[cluster % colors.length]; + let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)) : undefined; + // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document + set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); } - let set = this.sets.length > cluster ? this.sets[cluster] : undefined; - let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; - let clusterColor = colors[cluster % colors.length]; - set && set.filter(s => !s.isBackground).map(s => - s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor))); - set && set.filter(s => s.isBackground).map(s => - s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor))); - return clusterColor; } - return ""; + return clusterColor; } @action @@ -528,40 +490,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } let x = this.Document.panX || 0; let y = this.Document.panY || 0; - let docs = this.childDocs || []; + let docs = this.childLayoutPairs.map(pair => pair.layout); let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - // if (!this.isAnnotationOverlay) { - // PDFMenu.Instance.fadeOut(true); - // let minx = docs.length ? NumCast(docs[0].x) : 0; - // let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; - // let miny = docs.length ? NumCast(docs[0].y) : 0; - // let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; - // let ranges = docs.filter(doc => doc).reduce((range, doc) => { - // let x = NumCast(doc.x); - // let xe = x + NumCast(doc.width); - // let y = NumCast(doc.y); - // let ye = y + NumCast(doc.height); - // return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], - // [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; - // }, [[minx, maxx], [miny, maxy]]); - // let ink = Cast(this.fieldExtensionDoc.ink, InkField); - // if (ink && ink.inkData) { - // ink.inkData.forEach((value: StrokeData, key: string) => { - // let bounds = InkingCanvas.StrokeRect(value); - // ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; - // ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; - // }); - // } - - // let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(), - // this._pheight / this.zoomScaling()); - // let panelwidth = panelDim[0]; - // let panelheight = panelDim[1]; - // if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2; - // if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2; - // if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2; - // if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2; - // } + if (!this.isAnnotationOverlay) { + PDFMenu.Instance.fadeOut(true); + let minx = docs.length ? NumCast(docs[0].x) : 0; + let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; + let miny = docs.length ? NumCast(docs[0].y) : 0; + let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + let ranges = docs.filter(doc => doc).reduce((range, doc) => { + let x = NumCast(doc.x); + let xe = x + NumCast(doc.width); + let y = NumCast(doc.y); + let ye = y + NumCast(doc.height); + return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], + [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; + }, [[minx, maxx], [miny, maxy]]); + let ink = Cast(this.fieldExtensionDoc.ink, InkField); + if (ink && ink.inkData) { + ink.inkData.forEach((value: StrokeData, key: string) => { + let bounds = InkingCanvas.StrokeRect(value); + ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; + ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; + }); + } + + let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; + let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling() * cscale, + this._pheight / this.zoomScaling() * cscale); + if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; + if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; + if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; + if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; + } this.setPan(x - dx, y - dy); this._lastX = e.pageX; this._lastY = e.pageY; @@ -575,31 +536,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (BoolCast(this.props.Document.lockedPosition)) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); - return; } - - let childSelected = this.childDocs.some(doc => { - var dv = DocumentManager.Instance.getDocumentView(doc); - return dv && SelectionManager.IsSelected(dv) ? true : false; - }); - if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) { - return; - } - e.stopPropagation(); - - // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults. need more logic to fix. - // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) { - // let deltaScale = (1 - (e.deltaY / coefficient)); - // let nw = this.nativeWidth * deltaScale; - // let nh = this.nativeHeight * deltaScale; - // if (nw && nh) { - // this.props.Document.nativeWidth = nw; - // this.props.Document.nativeHeight = nh; - // } - // e.preventDefault(); - // } - // else - { + else if (this.props.active()) { + e.stopPropagation(); let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); @@ -628,46 +567,38 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action - onDrop = (e: React.DragEvent): void => { + onDrop = async (e: React.DragEvent): Promise => { var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - super.onDrop(e, { x: pt[0], y: pt[1] }); - } - - onDragOver = (): void => { + await super.onDrop(e, { x: pt[0], y: pt[1] }); } bringToFront = (doc: Doc, sendToBack?: boolean) => { if (sendToBack || doc.isBackground) { doc.zIndex = 0; - return; } - const docs = this.childDocs; - docs.slice().sort((doc1, doc2) => { - if (doc1 === doc) return 1; - if (doc2 === doc) return -1; - return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); - }).forEach((doc, index) => doc.zIndex = index + 1); - doc.zIndex = docs.length + 1; + else { + const docs = this.childLayoutPairs.map(pair => pair.layout); + docs.slice().sort((doc1, doc2) => { + if (doc1 === doc) return 1; + if (doc2 === doc) return -1; + return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); + }).forEach((doc, index) => doc.zIndex = index + 1); + doc.zIndex = docs.length + 1; + } } - focusDocument = (doc: Doc, willZoom: boolean, scale?: number) => { - const panX = this.Document.panX; - const panY = this.Document.panY; - const id = this.Document[Id]; + focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { const state = HistoryUtil.getState(); - state.initializers = state.initializers || {}; // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state - if (state.type === "doc" && panX !== undefined && panY !== undefined) { - const init = state.initializers[id]; + if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) { + const init = state.initializers![this.Document[Id]]; if (!init) { - state.initializers[id] = { - panX, panY - }; + state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY }; HistoryUtil.pushState(state); - } else if (init.panX !== panX || init.panY !== panY) { - init.panX = panX; - init.panY = panY; + } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) { + init.panX = this.Document.panX; + init.panY = this.Document.panY; HistoryUtil.pushState(state); } } @@ -675,8 +606,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; const newState = HistoryUtil.getState(); - (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY }; + newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); + + let px = this.Document.panX; + let py = this.Document.panY; + let s = this.Document.scale; this.setPan(newPanX, newPanY); this.props.Document.panTransformType = "Ease"; @@ -684,7 +619,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (willZoom) { this.setScaleToZoom(doc, scale); } - + console.log("Focused " + this.Document.title + " " + s); + afterFocus && setTimeout(() => { + if (afterFocus && afterFocus()) { + console.log("UnFocused " + this.Document.title + " " + s); + this.Document.panX = px; + this.Document.panY = py; + this.Document.scale = s; + } + }, 1000); } setScaleToZoom = (doc: Doc, scale: number = 0.5) => { @@ -716,13 +659,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, - onClick: this.props.onClick, + ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves + onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, PanelWidth: childLayout[WidthSym], PanelHeight: childLayout[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, + ContainingCollectionDoc: this.props.CollectionView.props.Document, focus: this.focusDocument, backgroundColor: this.getClusterColor, parentActive: this.props.active, @@ -741,6 +686,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, + ruleProvider: this.props.ruleProvider, onClick: this.props.onClick, ScreenToLocalTransform: this.getTransform, renderDepth: this.props.renderDepth, @@ -748,6 +694,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, + ContainingCollectionDoc: this.props.CollectionView.props.Document, focus: this.focusDocument, backgroundColor: returnEmptyString, parentActive: this.props.active, @@ -760,13 +707,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }; } - getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, state?: any } { - const result = script.script.run(params); - if (!result.success) { - return {}; + getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } { + const script = this.Document.arrangeScript; + const result = script && script.script.run(params, console.log); + if (result && result.success) { + return { ...result, transition: "transform 1s" }; } - let doc = params.doc; - return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result; + return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(params.doc.width, "number"), height: Cast(params.doc.height, "number") }; } viewDefsToJSX = (views: PivotView.PivotData[]) => { @@ -829,14 +776,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (this.Document.usePivotLayout) return PivotView.elements(this); let curPage = FieldValue(this.Document.curPage, -1); const initScript = this.Document.arrangeInit; - const script = this.Document.arrangeScript; let state: any = undefined; - let docs = this.childDocs; - let overlayDocs = DocListCast(this.props.Document.localOverlays); - overlayDocs && docs.push(...overlayDocs); + let pairs = this.childLayoutPairs; let elements: ViewDefResult[] = []; if (initScript) { - const initResult = initScript.script.run({ docs, collection: this.Document }); + const initResult = initScript.script.run({ docs: pairs.map(pair => pair.layout), collection: this.Document }, console.log); if (initResult.success) { const result = initResult.result; const { state: scriptState, views } = result; @@ -844,25 +788,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { elements = this.viewDefsToJSX(views); } } - let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { - var page = NumCast(doc.page, -1); - if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { - let minim = BoolCast(doc.isMinimized); - if (minim === undefined || !minim) { - const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : - { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") }; - state = pos.state === undefined ? state : pos.state; - let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, doc); - if (pair.layout && !(pair.data instanceof Promise)) { - prev.push({ - ele: , - bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } - }); - } - } + let docviews = pairs.reduce((prev, pair) => { + var page = NumCast(pair.layout.page, -1); + if (!pair.layout.isMinimized && ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1)) { + const pos = this.getCalculatedPositions({ doc: pair.layout, index: prev.length, collection: this.Document, docs: pairs.map(pair => pair.layout), state }); + state = pos.state === undefined ? state : pos.state; + prev.push({ + ele: , + bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: pos.width || 0, height: pos.height || 0 } + }); } return prev; }, elements); @@ -872,22 +810,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed.struct get views() { - let source = this.elements; - return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); } - @action onCursorMove = (e: React.PointerEvent) => { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox; - arrangeContents = async () => { const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); UndoManager.RunInBatch(() => { @@ -912,98 +846,86 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }, "arrange contents"); } + autoFormat = () => { + this.Document.isRuleProvider = !this.Document.isRuleProvider; + // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading. + this.Document.isRuleProvider && this.childLayoutPairs.map(pair => + // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template) + DocListCast(pair.layout.layout instanceof Doc ? pair.layout.layout.data : pair.layout.data).map(heading => { + let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading); + let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout; + if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) { + Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor; + } + }) + ); + } + analyzeStrokes = async () => { - let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); - if (!data) { - return; + let data = Cast(this.fieldExtensionDoc.ink, InkField); + if (data) { + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData); } - let relevantKeys = ["inkAnalysis", "handwriting"]; - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData); } onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; - if (this.childDocs.some(d => d.isTemplate)) { - layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); + if (this.childDocs.some(d => BoolCast(d.isTemplate))) { + layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); } layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); - layoutItems.push({ - description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`, - event: async () => { - Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes - Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white"; - this.props.Document.useClusters = !this.props.Document.useClusters; - this.updateClusters(); - }, - icon: !this.props.Document.useClusters ? "braille" : "braille" - }); - this.props.Document.useClusters && layoutItems.push({ - description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`, - event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground, - icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard" - }); + layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); + layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" }); layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" }); layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); layoutItems.push({ - description: "Import document", icon: "upload", event: () => { + description: "Import document", icon: "upload", event: ({ x, y }) => { const input = document.createElement("input"); input.type = "file"; input.accept = ".zip"; input.onchange = async _e => { - const files = input.files; - if (!files) return; - const file = files[0]; - let formData = new FormData(); - formData.append('file', file); - formData.append('remap', "true"); const upload = Utils.prepend("/uploadDoc"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json === "error") { - return; - } - const doc = await DocServer.GetRefField(json); - if (!doc || !(doc instanceof Doc)) { - return; + let formData = new FormData(); + const file = input.files && input.files[0]; + if (file) { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument && this.props.addDocument(doc, false); + } + } } - const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY); - doc.x = x, doc.y = y; - this.props.addDocument && - this.props.addDocument(doc, false); }; input.click(); } }); - let noteItems: ContextMenuProps[] = []; - if (CurrentUserUtils.UserDocument) { - let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); - notes.map((node, i) => noteItems.push({ description: (i + 1) + ": " + StrCast(node.title), event: () => this.createText(i), icon: "eye" })); - } - layoutItems.push({ description: "Add Note ...", subitems: noteItems, icon: "eye" }); + layoutItems.push({ + description: "Add Note ...", + subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({ + description: (i + 1) + ": " + StrCast(note.title), + event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })), + icon: "eye" + })) as ContextMenuProps[], + icon: "eye" + }); ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" }); } - createText = (noteStyle: number) => { - let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY); - if (CurrentUserUtils.UserDocument) { - let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); - let text = Docs.Create.TextDocument({ width: 200, height: 100, x: pt[0], y: pt[1], autoHeight: true, title: StrCast(notes[noteStyle % notes.length].title) }); - text.layout = notes[noteStyle % notes.length]; - this.addLiveTextBox(text); - } - } private childViews = () => [ , ...this.views ] - private overlayChildViews = () => { - return [...this.overlayViews]; - } public static AddCustomLayout(doc: Doc, dataKey: string): () => void { return () => { @@ -1034,15 +956,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } render() { + // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x; this.props.Document.fitY = this.actualContentBounds && this.actualContentBounds.y; this.props.Document.fitW = this.actualContentBounds && (this.actualContentBounds.r - this.actualContentBounds.x); this.props.Document.fitH = this.actualContentBounds && (this.actualContentBounds.b - this.actualContentBounds.y); + // if fieldExt is set, then children will be stored in the extension document for the fieldKey. + // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document + Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey); const easing = () => this.props.Document.panTransformType === "Ease"; - Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); return (
    + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}> @@ -1056,7 +981,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { - {this.overlayChildViews()} + {this.overlayViews}
    ); @@ -1077,7 +1002,6 @@ class CollectionFreeFormOverlayView extends React.Component boolean }> { @computed get backgroundView() { - let props = this.props; return (); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index fe48a3485..bbea4a555 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,28 +1,24 @@ -import * as htmlToImage from "html-to-image"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, FieldResult, DocListCast } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; +import { Doc, DocListCast } from "../../../../new_fields/Doc"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; +import { listSpec } from "../../../../new_fields/Schema"; +import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; +import { ComputedField } from "../../../../new_fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; -import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; -import { Templates } from "../../Templates"; import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); -import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField"; -import { string } from "prop-types"; -import { listSpec } from "../../../../new_fields/Schema"; -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -232,18 +228,14 @@ export class MarqueeView extends React.Component return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; } - get ink() { - let container = this.props.container.props.Document; - let containerKey = this.props.container.props.fieldKey; - let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true"); - return Cast(extensionDoc.ink, InkField); + get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. + let cprops = this.props.container.props; + return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField); } set ink(value: InkField | undefined) { - let container = Doc.GetProto(this.props.container.props.Document); - let containerKey = this.props.container.props.fieldKey; - let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true"); - extensionDoc.ink = value; + let cprops = this.props.container.props; + Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value; } @undoBatch @@ -287,7 +279,7 @@ export class MarqueeView extends React.Component let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]); let usedPaletted = new Map(); [...this.props.activeDocuments(), this.props.container.props.Document].map(child => { - let bg = StrCast(child.backgroundColor); + let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor); if (palette.indexOf(bg) !== -1) { palette.splice(palette.indexOf(bg), 1); if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); @@ -309,13 +301,13 @@ export class MarqueeView extends React.Component defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor, width: bounds.width, height: bounds.height, - title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection", + title: "a nested collection", }); let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; this.marqueeInkDelete(inkData); - if (e.key === "s") { + if (e.key === "s" || e.key === "S") { selected.map(d => { this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; @@ -324,39 +316,23 @@ export class MarqueeView extends React.Component return d; }); newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - newCollection.proto!.summaryDoc = summary; - selected = [newCollection]; + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + Doc.GetProto(summary).summarizedDocs = new List([newCollection]); newCollection.x = bounds.left + bounds.width; - summary.proto!.subBulletDocs = new List(selected); - summary.templates = new List([Templates.Bullet.Layout]); - let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); - container.viewType = CollectionViewType.Stacking; - container.autoHeight = true; - this.props.addLiveTextDocument(container); - // }); - } else if (e.key === "S") { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; - return d; - }); - newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - newCollection.proto!.summaryDoc = summary; - selected = [newCollection]; - newCollection.x = bounds.left + bounds.width; - //this.props.addDocument(newCollection, false); - summary.proto!.summarizedDocs = new List(selected); - summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" - summary.autoHeight = true; - - this.props.addLiveTextDocument(summary); + Doc.GetProto(newCollection).summaryDoc = summary; + Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); + if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); + container.viewType = CollectionViewType.Stacking; + container.autoHeight = true; + Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" + this.props.addLiveTextDocument(container); + } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them + Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" + this.props.addLiveTextDocument(summary); + } } else { - newCollection.ruleProvider = this.props.container.props.Document; this.props.addDocument(newCollection, false); this.props.selectDocuments([newCollection]); } diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx index 9931515f3..868afc423 100644 --- a/src/client/views/document_templates/image_card/ImageCard.tsx +++ b/src/client/views/document_templates/image_card/ImageCard.tsx @@ -1,8 +1,5 @@ import * as React from 'react'; -import { DocComponent } from '../../DocComponent'; import { FieldViewProps } from '../../nodes/FieldView'; -import { createSchema, makeInterface } from '../../../../new_fields/Schema'; -import { createInterface } from 'readline'; import { ImageBox } from '../../nodes/ImageBox'; export default class ImageCard extends React.Component { diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx index f8807641b..81b0249dd 100644 --- a/src/client/views/linking/LinkFollowBox.tsx +++ b/src/client/views/linking/LinkFollowBox.tsx @@ -2,7 +2,7 @@ import { observable, computed, action, runInAction, reaction, IReactionDisposer import React = require("react"); import { observer } from "mobx-react"; import { FieldViewProps, FieldView } from "../nodes/FieldView"; -import { Doc, DocListCastAsync } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc"; import { undoBatch } from "../../util/UndoManager"; import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; import { CollectionViewType } from "../collections/CollectionBaseView"; @@ -85,11 +85,10 @@ export class LinkFollowBox extends React.Component { } async resetPan() { - if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionView) { - let colDoc = this.sourceView.props.ContainingCollectionView.props.Document; - runInAction(() => { this.canPan = false; }); - if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) { - let docs = Cast(colDoc.data, listSpec(Doc), []); + if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + runInAction(() => this.canPan = false); + if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) { + let docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []); let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc)); aliases.forEach(alias => { @@ -172,7 +171,6 @@ export class LinkFollowBox extends React.Component { if (LinkFollowBox.destinationDoc) { let view: DocumentView | null = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc); view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); - SelectionManager.DeselectAll(); } } @@ -188,7 +186,6 @@ export class LinkFollowBox extends React.Component { let view = DocumentManager.Instance.getDocumentView(options.context); view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); this.highlightDoc(); - SelectionManager.DeselectAll(); } } @@ -198,9 +195,9 @@ export class LinkFollowBox extends React.Component { } - _addDocTab: (undefined | ((doc: Doc, dataDoc: Doc | undefined, where: string) => void)); + _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt, where: string) => boolean)); - setAddDocTab = (addFunc: (doc: Doc, dataDoc: Doc | undefined, where: string) => void) => { + setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt, where: string) => boolean) => { this._addDocTab = addFunc; } @@ -214,7 +211,7 @@ export class LinkFollowBox extends React.Component { options.context.panX = newPanX; options.context.panY = newPanY; } - CollectionDockingView.Instance.AddRightSplit(options.context, undefined); + (this._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight"); if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); @@ -227,7 +224,7 @@ export class LinkFollowBox extends React.Component { openLinkRight = () => { if (LinkFollowBox.destinationDoc) { let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc); - CollectionDockingView.Instance.AddRightSplit(alias, undefined); + (this._addDocTab || this.props.addDocTab)(alias, undefined, "onRight"); this.highlightDoc(); SelectionManager.DeselectAll(); } @@ -247,7 +244,7 @@ export class LinkFollowBox extends React.Component { let sourceContext = await Cast(proto.sourceContext, Doc); const shouldZoom = options ? options.shouldZoom : false; - let dockingFunc = (document: Doc) => { this._addDocTab && this._addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; + let dockingFunc = (document: Doc) => { (this._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) { DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext); @@ -274,7 +271,7 @@ export class LinkFollowBox extends React.Component { if (LinkFollowBox.destinationDoc) { let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc); // this.prosp.addDocTab is empty -- use the link source's addDocTab - this._addDocTab && this._addDocTab(fullScreenAlias, undefined, "inTab"); + (this._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab"); this.highlightDoc(); SelectionManager.DeselectAll(); @@ -291,7 +288,7 @@ export class LinkFollowBox extends React.Component { options.context.panX = newPanX; options.context.panY = newPanY; } - this._addDocTab && this._addDocTab(options.context, undefined, "inTab"); + (this._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab"); if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); this.highlightDoc(); @@ -373,9 +370,9 @@ export class LinkFollowBox extends React.Component { this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN); if (this.shouldUseOnlyParentContext) { - if (this.sourceView && this.sourceView.props.ContainingCollectionView) { - this.selectedContext = this.sourceView.props.ContainingCollectionView.props.Document; - this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionView.props.Document.title)); + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + this.selectedContext = this.sourceView.props.ContainingCollectionDoc; + this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionDoc.title)); } } } @@ -396,9 +393,8 @@ export class LinkFollowBox extends React.Component { @computed get canOpenInPlace() { - if (this.sourceView && this.sourceView.props.ContainingCollectionView) { - let colView = this.sourceView.props.ContainingCollectionView; - let colDoc = colView.props.Document; + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + let colDoc = this.sourceView.props.ContainingCollectionDoc; if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true; } return false; @@ -459,17 +455,15 @@ export class LinkFollowBox extends React.Component { @computed get parentName() { - if (this.sourceView && this.sourceView.props.ContainingCollectionView) { - let colView = this.sourceView.props.ContainingCollectionView; - return colView.props.Document.title; + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + return this.sourceView.props.ContainingCollectionDoc.title; } } @computed get parentID(): string { - if (this.sourceView && this.sourceView.props.ContainingCollectionView) { - let colView = this.sourceView.props.ContainingCollectionView; - return StrCast(colView.props.Document[Id]); + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + return StrCast(this.sourceView.props.ContainingCollectionDoc[Id]); } return "col"; } diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 842ce45b1..27af873b5 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -16,7 +16,7 @@ library.add(faTrash); interface Props { docView: DocumentView; changeFlyout: () => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; } @observer diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index b6a24b0c8..1891919ce 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,27 +1,25 @@ -import { action, observable } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; import { observer } from "mobx-react"; -import { DocumentView } from "../nodes/DocumentView"; -import { LinkMenuItem } from "./LinkMenuItem"; -import { LinkEditor } from "./LinkEditor"; -import './LinkMenu.scss'; -import React = require("react"); -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import { LinkManager } from "../../util/LinkManager"; -import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager"; +import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { emptyFunction } from "../../../Utils"; import { Docs } from "../../documents/Documents"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { DragManager, SetupDrag } from "../../util/DragManager"; +import { LinkManager } from "../../util/LinkManager"; import { UndoManager } from "../../util/UndoManager"; -import { StrCast } from "../../../new_fields/Types"; -import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField"; +import { DocumentView } from "../nodes/DocumentView"; +import './LinkMenu.scss'; +import { LinkMenuItem } from "./LinkMenuItem"; +import React = require("react"); interface LinkMenuGroupProps { sourceDoc: Doc; group: Doc[]; groupType: string; showEditor: (linkDoc: Doc) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; docView: DocumentView; } @@ -57,7 +55,7 @@ export class LinkMenuGroup extends React.Component { let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); if (opp) return opp; }) as Doc[]; - let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined)); + let dragData = new DragManager.DocumentDragData(draggedDocs); DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, { handlers: { diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 19a0023e9..82fe3df23 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -21,7 +21,7 @@ interface LinkMenuItemProps { sourceDoc: Doc; destinationDoc: Doc; showEditor: (linkDoc: Doc) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; } @observer diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index 68d3b8ae1..f08ea4891 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -3,7 +3,7 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCastAsync } from '../../../new_fields/Doc'; +import { Doc, DocListCastAsync, DocListCast } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; import { ScriptField } from '../../../new_fields/ScriptField'; @@ -49,7 +49,7 @@ export class ButtonBox extends DocComponent(Butt funcs.push({ description: "Clear Script Params", event: () => { let params = Cast(this.props.Document.buttonParams, listSpec("string")); - params && params.map(p => this.props.Document[p] = undefined) + params && params.map(p => this.props.Document[p] = undefined); }, icon: "trash" }); @@ -68,7 +68,7 @@ export class ButtonBox extends DocComponent(Butt render() { let params = Cast(this.props.Document.buttonParams, listSpec("string")); let missingParams = params && params.filter(p => this.props.Document[p] === undefined); - params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ... + params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... return (
    diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss new file mode 100644 index 000000000..c0d9e1267 --- /dev/null +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -0,0 +1,5 @@ +.collectionFreeFormDocumentView-container { + transform-origin: left top; + position: absolute; + background-color: transparent; +} \ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 07dd1cae7..9685f9bca 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,13 +1,14 @@ -import { computed } from "mobx"; +import { computed, action, observable, reaction, IReactionDisposer, trace } from "mobx"; import { observer } from "mobx-react"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types"; +import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types"; import { Transform } from "../../util/Transform"; import { DocComponent } from "../DocComponent"; -import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; -import "./DocumentView.scss"; +import { percent2frac } from "../../../Utils"; +import { DocumentView, DocumentViewProps, documentSchema } from "./DocumentView"; +import "./CollectionFreeFormDocumentView.scss"; import React = require("react"); -import { Doc } from "../../../new_fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { random } from "animejs"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @@ -16,31 +17,34 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { width?: number; height?: number; jitterRotation: number; + transition?: string; } - -const schema = createSchema({ +export const positionSchema = createSchema({ zIndex: "number", + x: "number", + y: "number", + z: "number", }); -//TODO Types: The import order is wrong, so positionSchema is undefined -type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>; -const FreeformDocument = makeInterface(schema, positionSchema); +export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>; +export const PositionDocument = makeInterface(documentSchema, positionSchema); @observer -export class CollectionFreeFormDocumentView extends DocComponent(FreeformDocument) { +export class CollectionFreeFormDocumentView extends DocComponent(PositionDocument) { + _disposer: IReactionDisposer | undefined = undefined; @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; } - @computed get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; } - @computed get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; } - @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.Document.width || 0; } - @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.Document.height || 0; } - @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); } - @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); } - @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); } + @computed get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; } + @computed get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; } + @computed get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.Document[WidthSym](); } + @computed get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.Document[HeightSym](); } + @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } + @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } + @computed get scaleToOverridingWidth() { return this.width / FieldValue(this.Document.width, this.width); } @computed get renderScriptDim() { if (this.Document.renderScript) { - let someView = Cast(this.Document.someView, Doc); - let minimap = Cast(this.Document.minimap, Doc); + let someView = Cast(this.props.Document.someView, Doc); + let minimap = Cast(this.props.Document.minimap, Doc); if (someView instanceof Doc && minimap instanceof Doc) { let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2; let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2; @@ -52,34 +56,31 @@ export class CollectionFreeFormDocumentView extends DocComponent this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1; + componentWillUnmount() { + this._disposer && this._disposer(); + } + componentDidMount() { + this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating], + () => { + const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined; + this._animPos = !target ? undefined : target[2] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]); + }, { fireImmediately: true }); + } + + contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth) - animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => { - this.props.bringToFront(this.props.Document); - let targetPos = [this.Document.x || 0, this.Document.y || 0]; - let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]); - DocumentView.animateBetweenIconFunc(this.props.Document, - this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => { - let pval = maximizing ? - [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] : - [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress]; - this.Document.x = progress === 1 ? targetPos[0] : pval[0]; - this.Document.y = progress === 1 ? targetPos[1] : pval[1]; - }); - } - borderRounding = () => { - let br = StrCast(this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout.borderRounding : this.props.Document.borderRounding); + let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; + let br = StrCast(((this.layoutDoc.layout as Doc) || this.Document).borderRounding); + br = !br && ruleRounding ? ruleRounding : br; if (br.endsWith("%")) { - let percent = Number(br.substr(0, br.length - 1)) / 100; let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight)); - let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight())); - return minDim; + return percent2frac(br) * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight())); } return undefined; } @@ -95,24 +96,21 @@ export class CollectionFreeFormDocumentView extends DocComponent
    ); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index d0e117fe4..3c3cc0d91 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -93,13 +93,6 @@ export class DocumentContentsView extends React.Component { - let field = this.props.Document.templates; - if (field && field instanceof List) { - return field; - } - return new List(); - } @computed get finalLayout() { return this.props.layoutKey === "overlayLayout" ? "
    " : this.layout; } @@ -107,7 +100,7 @@ export class DocumentContentsView extends React.Component 7) return (null); - if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); + if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null); return ; -// const LinkDoc = makeInterface(linkSchema); - export interface DocumentViewProps { ContainingCollectionView: Opt; + ContainingCollectionDoc: Opt; Document: Doc; DataDoc?: Doc; fitToBox?: boolean; @@ -93,47 +83,51 @@ export interface DocumentViewProps { renderDepth: number; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; ContentScaling: () => number; + ruleProvider: Doc | undefined; PanelWidth: () => number; PanelHeight: () => number; - focus: (doc: Doc, willZoom: boolean, scale?: number) => void; + focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; - collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; backgroundColor: (doc: Doc) => string | undefined; getScale: () => number; - animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; + animateBetweenIcon?: (maximize: boolean, target: number[]) => void; ChromeHeight?: () => number; } -const schema = createSchema({ - layout: "string", - nativeWidth: "number", - nativeHeight: "number", - backgroundColor: "string", - opacity: "number", - hidden: "boolean", - onClick: ScriptField, -}); - -export const positionSchema = createSchema({ - nativeWidth: "number", - nativeHeight: "number", - width: "number", - height: "number", - x: "number", - y: "number", - z: "number", +export const documentSchema = createSchema({ + // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out + title: "string", // document title (can be on either data document or layout) + nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set + nativeHeight: "number", // " + width: "number", // width of document in its container's coordinate system + height: "number", // " + backgroundColor: "string", // background color of document + opacity: "number", // opacity of document + onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) + ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document + autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents + isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed + isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) + type: "string", // enumerated type of document + maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) + lockedPosition: "boolean", // whether the document can be spatially manipulated + borderRounding: "string", // border radius rounding of document + searchFields: "string", // the search fields to display when this document matches a search in its metadata + heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc) + showCaption: "string", // whether editable caption text is overlayed at the bottom of the document + showTitle: "string", // whether an editable title banner is displayed at tht top of the document + isButton: "boolean", // whether document functions as a button (overiding native interactions of its content) + ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) }); -export type PositionDocument = makeInterface<[typeof positionSchema]>; -export const PositionDocument = makeInterface(positionSchema); -type Document = makeInterface<[typeof schema]>; -const Document = makeInterface(schema); +type Document = makeInterface<[typeof documentSchema]>; +const Document = makeInterface(documentSchema); @observer export class DocumentView extends DocComponent(Document) { @@ -141,107 +135,41 @@ export class DocumentView extends DocComponent(Docu private _downY: number = 0; private _lastTap: number = 0; private _doubleTap = false; - private _hitExpander = false; private _hitTemplateDrag = false; private _mainCont = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; - _animateToIconDisposer?: IReactionDisposer; - _reactionDisposer?: IReactionDisposer; public get ContentDiv() { return this._mainCont.current; } - @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } - @computed get topMost(): boolean { return this.props.renderDepth === 0; } - screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); } + @computed get topMost() { return this.props.renderDepth === 0; } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } @action componentDidMount() { - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { - handlers: { drop: this.drop.bind(this) } - }); - } - // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes - this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title), - this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], - () => { - let maxDoc = DocListCast(this.props.Document.maximizedDocs); - if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { - this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon"; - } - let sumDoc = Cast(this.props.Document.summaryDoc, Doc); - if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) { - this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; - } - }, { fireImmediately: true }); - this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => - (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false) - , { fireImmediately: true }); + this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); DocumentManager.Instance.DocumentViews.push(this); } - animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => { - this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) : - DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing); - } - - public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => { - setTimeout(() => { - let now = Date.now(); - let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1; - doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - cb && cb(progress); - if (now < stime + 200) { - DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb); - } - else { - doc.isMinimized = !maximizing; - doc.isIconAnimating = undefined; - } - doc.willMaximize = false; - }, - 2); - } @action componentDidUpdate() { this._dropDisposer && this._dropDisposer(); - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { - handlers: { drop: this.drop.bind(this) } - }); - } + this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); } + @action componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - this._animateToIconDisposer && this._animateToIconDisposer(); this._dropDisposer && this._dropDisposer(); DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } - stopPropagation = (e: React.SyntheticEvent) => { - e.stopPropagation(); - } - - get dataDoc() { - if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) { - // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document - // has a template layout document, then we will render the template layout but use - // this document as the data document for the layout. - return this.props.Document; - } - return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; - } - startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) { if (this._mainCont.current) { - let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; - let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; + let dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected); - const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.xOffset = xoff; - dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { @@ -252,151 +180,79 @@ export class DocumentView extends DocComponent(Docu }); } } - toggleMinimized = async () => { - let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); - if (minimizedDoc) { - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint( - NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y)); - this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs)); - } - } - - static _undoBatch?: UndoManager.Batch = undefined; - @action - public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - if (!DocumentView._undoBatch) { - DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); - } - let isMinimized: boolean | undefined; - expandedDocs.map(maximizedDoc => { - let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized); - } - maximizedDoc.willMaximize = isMinimized; - maximizedDoc.isMinimized = false; - maximizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); - } - }); - setTimeout(() => { - DocumentView._undoBatch && DocumentView._undoBatch.end(); - DocumentView._undoBatch = undefined; - }, 500); - } - } onClick = async (e: React.MouseEvent) => { - if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing. - if (this.onClickHandler && this.onClickHandler.script) { + if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { e.stopPropagation(); - this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }); e.preventDefault(); - return; + if (this._doubleTap && this.props.renderDepth) { + let fullScreenAlias = Doc.MakeAlias(this.props.Document); + let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); + if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { + await swapViews(fullScreenAlias, "layoutCustom", "layoutNative"); + } + this.props.addDocTab(fullScreenAlias, undefined, "inTab"); + SelectionManager.DeselectAll(); + Doc.UnBrushDoc(this.props.Document); + } else if (this.onClickHandler && this.onClickHandler.script) { + this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + } else if (this.Document.isButton) { + SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. + this.buttonClick(e.altKey, e.ctrlKey); + } else SelectionManager.SelectDoc(this, e.ctrlKey); } - let altKey = e.altKey; - let ctrlKey = e.ctrlKey; - if (this._doubleTap && this.props.renderDepth) { - e.stopPropagation(); - let fullScreenAlias = Doc.MakeAlias(this.props.Document); - fullScreenAlias.templates = new List(); - Doc.UseDetailLayout(fullScreenAlias); - fullScreenAlias.showCaption = true; - this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); + } + + buttonClick = async (altKey: boolean, ctrlKey: boolean) => { + let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); + let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); + let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); + let expandedDocs: Doc[] = []; + expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; + expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; + // let expandedDocs = [ ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; + if (expandedDocs.length) { SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); - } - else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && - (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { - if (BoolCast(this.props.Document.ignoreClick)) { - return; + let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace"); + maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab")); + if (maxLocation === "inPlace") { + expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc, false)); + let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); + DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs); + } else { + expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation))); } - e.stopPropagation(); - SelectionManager.SelectDoc(this, e.ctrlKey); - let isExpander = (e.target as any).id === "isExpander"; - if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) { - let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); - let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); - let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); - let expandedDocs: Doc[] = []; - expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; - expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; - expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; - // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; - if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents - SelectionManager.DeselectAll(); - let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); - let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); - if (altKey || ctrlKey) { - maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace")); - if (!maxLocation || maxLocation === "inPlace") { - let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized), false); - expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false); - let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - if (!hasView) { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false)); - } - expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized); - } - } - if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) { - let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); - if (dataDocs) { - expandedDocs.forEach(maxDoc => - (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && - this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation))); - } - } else { - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); - this.collapseTargetsToPoint(scrpt, expandedDocs); - } - } - else if (linkedDocs.length) { - SelectionManager.DeselectAll(); - let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document)); - let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]]; - - // @TODO: shouldn't always follow target context - let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; - - let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined]; - - if (!linkedFwdDocs.some(l => l instanceof Promise)) { - let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab"); - let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, - document => { // open up target if it's not already in view ... - let cv = this.props.ContainingCollectionView; // bcz: ugh --- maybe need to have a props.unfocus() method so that we leave things in the state we found them?? - let px = cv && cv.props.Document.panX; - let py = cv && cv.props.Document.panY; - let s = cv && cv.props.Document.scale; - this.props.focus(this.props.Document, true, 1); // by zooming into the button document first - setTimeout(() => { - this.props.addDocTab(document, undefined, maxLocation); - cv && (cv.props.Document.panX = px); - cv && (cv.props.Document.panY = py); - cv && (cv.props.Document.scale = s); - }, 1000); // then after the 1sec animation, open up the target in a new tab - }, - linkedFwdPage[altKey ? 1 : 0], targetContext); - } - } + } + else if (linkedDocs.length) { + SelectionManager.DeselectAll(); + let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored); + let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); + if (firstUnshown.length) first = [firstUnshown[0]]; + let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]]; + + // @TODO: shouldn't always follow target context + let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; + let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined]; + + if (!linkedFwdDocs.some(l => l instanceof Promise)) { + let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab"); + let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, + // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards + doc => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)), + linkedFwdPage[altKey ? 1 : 0], targetContext); } } } - onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble) return; this._downX = e.clientX; this._downY = e.clientY; - this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; this._hitTemplateDrag = false; + // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where + // this document is the template and we apply it to whatever we drop it on. for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { this._hitTemplateDrag = true; @@ -413,11 +269,11 @@ export class DocumentView extends DocComponent(Docu document.removeEventListener("pointermove", this.onPointerMove); } else if (!e.cancelBubble && this.active) { - if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) { - if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.Document.lockedPosition)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -435,77 +291,72 @@ export class DocumentView extends DocComponent(Docu deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } @undoBatch - fieldsClicked = (): void => { - let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); - this.props.addDocTab(kvp, this.dataDoc, "onRight"); - } + static makeNativeViewClicked = (doc: Doc): void => { swapViews(doc, "layoutNative", "layoutCustom"); } @undoBatch - makeBtnClicked = (): void => { - let doc = Doc.GetProto(this.props.Document); - if (doc.isButton || doc.onClick) { - doc.isButton = false; - doc.onClick = undefined; + static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt) => { + if (doc.layoutCustom === undefined) { + Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative"); + await swapViews(doc, "", "layoutNative"); + + const width = NumCast(doc.width); + const height = NumCast(doc.height); + const options = { title: "data", width, x: -width / 2, y: - height / 2, }; + let fieldTemplate = doc.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : + doc.type === DocumentType.VID ? Docs.Create.VideoDocument("http://www.cs.brown.edu", options) : + Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + + fieldTemplate.backgroundColor = doc.backgroundColor; + fieldTemplate.heading = 1; + fieldTemplate.autoHeight = true; + + let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) }); + + Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true); + Doc.ApplyTemplateTo(docTemplate, doc, undefined); + Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom"); } else { - doc.isButton = true; + swapViews(doc, "layoutCustom", "layoutNative"); } - - // if (doc.isButton) { - // if (!doc.nativeWidth) { - // doc.nativeWidth = this.props.Document[WidthSym](); - // doc.nativeHeight = this.props.Document[HeightSym](); - // } - // } else { - // doc.nativeWidth = doc.nativeHeight = undefined; - // } } @undoBatch - public fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this); - SelectionManager.DeselectAll(); + makeBtnClicked = (): void => { + if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) { + this.Document.isButton = false; + this.Document.ignoreClick = false; + this.Document.onClick = undefined; + } else { + this.Document.isButton = true; + } } @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.AnnotationDragData) { + /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - let annotationDoc = de.data.annotationDocument; - annotationDoc.linkedToDoc = true; - de.data.targetContext = this.props.ContainingCollectionView!.props.Document; + let sourceDoc = de.data.annotationDocument; let targetDoc = this.props.Document; + let annotations = await DocListCastAsync(sourceDoc.annotations); + sourceDoc.linkedToDoc = true; + de.data.targetContext = this.props.ContainingCollectionDoc; targetDoc.targetContext = de.data.targetContext; - let annotations = await DocListCastAsync(annotationDoc.annotations); annotations && annotations.forEach(anno => anno.target = targetDoc); - DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`); + DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionDoc, `Link from ${StrCast(sourceDoc.title)}`); } if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc); + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document); e.stopPropagation(); } if (de.data instanceof DragManager.LinkDragData) { - let sourceDoc = de.data.linkSourceDocument; - let destDoc = this.props.Document; - e.stopPropagation(); - if (de.mods === "AltKey") { - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - let src = protoSrc ? protoSrc : sourceDoc; - let dst = protoDest ? protoDest : destDoc; - dst.data = (src.data! as ObjectField)[Copy](); - dst.nativeWidth = src.nativeWidth; - dst.nativeHeight = src.nativeHeight; - } - else { - // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); - // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); - de.data.droppedDocuments.push(destDoc); - de.data.linkDocument = linkDoc; - } + // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); + // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); + de.data.linkSourceDocument !== this.props.Document && + (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc)); } } @@ -513,9 +364,9 @@ export class DocumentView extends DocComponent(Docu onDrop = (e: React.DragEvent) => { let text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith("(Docu @undoBatch @action freezeNativeDimensions = (): void => { - let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); - this.props.Document.autoHeight = proto.autoHeight = false; - proto.ignoreAspect = !BoolCast(proto.ignoreAspect); - if (!BoolCast(proto.ignoreAspect) && !proto.nativeWidth) { + let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); + proto.autoHeight = this.Document.autoHeight = false; + proto.ignoreAspect = !proto.ignoreAspect; + if (!proto.ignoreAspect && !proto.nativeWidth) { proto.nativeWidth = this.props.PanelWidth(); proto.nativeHeight = this.props.PanelHeight(); } } + + @undoBatch + @action + makeIntoPortal = async () => { + let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc))); + if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) { + let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); + DocServer.GetRefField(portalID).then(existingPortal => { + let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID }); + DocUtils.MakeLink(this.props.Document, portal, undefined, portalID); + this.Document.isButton = true; + }); + } + } + @undoBatch @action - makeIntoPortal = (): void => { - if (!DocListCast(this.props.Document.links).find(doc => { - if (Cast(doc.anchor2, Doc) instanceof Doc && (Cast(doc.anchor2, Doc) as Doc).title === this.props.Document.title + ".portal") return true; - return false; - })) { - let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); - DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal"); - Doc.GetProto(this.props.Document).isButton = true; + setCustomView = (custom: boolean): void => { + if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) { + Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc); + } else { // bcz: not robust -- for now documents with string layout are native documents, and those with Doc layouts are customized + custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); } } @undoBatch @action makeBackground = (): void => { - this.layoutDoc.isBackground = !this.layoutDoc.isBackground; - this.layoutDoc.isBackground && this.props.bringToFront(this.layoutDoc, true); + this.Document.isBackground = !this.Document.isBackground; + this.Document.isBackground && this.props.bringToFront(this.Document, true); } @undoBatch @action toggleLockPosition = (): void => { - this.layoutDoc.lockedPosition = BoolCast(this.layoutDoc.lockedPosition) ? undefined : true; + this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true; } listen = async () => { @@ -570,48 +433,6 @@ export class DocumentView extends DocComponent(Docu }); } - public static makeNativeViewClicked = undoBatch((document: Doc): void => { - document.customLayout = document.layout; - document.layout = document.nativeLayout; - document.type = document.nativeType; - document.nativeWidth = document.nativeNativeWidth; - document.nativeHeight = document.nativeNativeHeight; - document.ignoreAspect = document.nativeIgnoreAspect; - document.nativeLayout = undefined; - document.nativeNativeWidth = undefined; - document.nativeNativeHeight = undefined; - document.nativeIgnoreAspect = undefined; - }); - - public static makeCustomViewClicked = undoBatch((document: Doc, showTitle = undefined): void => { - document.nativeLayout = document.layout; - document.nativeType = document.type; - document.nativeNativeWidth = document.nativeWidth; - document.nativeNativeHeight = document.nativeHeight; - document.nativeIgnoreAspect = document.ignoreAspect; - PromiseValue(Cast(document.customLayout, Doc)).then(custom => { - if (custom) { - document.type = DocumentType.TEMPLATE; - document.layout = custom; - !custom.nativeWidth && (document.nativeWidth = 0); - !custom.nativeHeight && (document.nativeHeight = 0); - !custom.nativeWidth && (document.ignoreAspect = true); - } else { - let options = { title: "data", width: NumCast(document.width), height: NumCast(document.height) + 25, x: -NumCast(document.width) / 2, y: -NumCast(document.height) / 2, }; - let fieldTemplate = document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : Docs.Create.ImageDocument("http://www.cs.brown.edu", options); - - let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(document.title) + "layout", width: NumCast(document.width) + 20, height: Math.max(100, NumCast(document.height) + 45) }); - let metaKey = "data"; - let proto = Doc.GetProto(docTemplate); - Doc.MakeTemplate(fieldTemplate, metaKey, proto); - fieldTemplate.showTitle = showTitle; - - Doc.ApplyTemplateTo(docTemplate, document, undefined, false); - document.customLayout = document.layout; - } - }); - }); - @action onContextMenu = async (e: React.MouseEvent): Promise => { e.persist(); @@ -625,13 +446,14 @@ export class DocumentView extends DocComponent(Docu const cm = ContextMenu.Instance; let subitems: ContextMenuProps[] = []; - subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" }); - subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); + subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" }); + subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); + if (Cast(this.props.Document.data, ImageField)) { cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); } @@ -640,25 +462,17 @@ export class DocumentView extends DocComponent(Docu cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } - let existingMake = ContextMenu.Instance.findByDescription("Make..."); - let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; - makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); - makes.push({ description: "Custom Document View", event: () => DocumentView.makeCustomViewClicked(this.props.Document), icon: "concierge-bell" }); - makes.push({ description: "Metadata Field View", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }); - makes.push({ description: "Into Portal", event: this.makeIntoPortal, icon: "window-restore" }); - makes.push({ description: this.layoutDoc.ignoreClick ? "Selectable" : "Unselectable", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" }); - !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); - let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: this.layoutDoc.ignoreClick ? "Select" : "Do Nothing", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" }); - onClicks.push({ description: this.props.Document.isButton || this.props.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" }); + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); onClicks.push({ description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => { - this.props.Document.collectionContext = this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; + this.props.Document.collectionContext = this.props.ContainingCollectionDoc; ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n"); } }); @@ -666,22 +480,19 @@ export class DocumentView extends DocComponent(Docu let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; - layoutItems.push({ description: this.props.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); - if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.layout instanceof Doc) { - layoutItems.push({ description: "Make View of Metadata Field", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }); + layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" }); + if (this.props.DataDoc) { + layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" }); } - layoutItems.push({ description: `${this.layoutDoc.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.layoutDoc.chromeStatus = (this.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - layoutItems.push({ description: `${this.layoutDoc.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" }); - layoutItems.push({ description: this.props.Document.ignoreAspect || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); - layoutItems.push({ description: this.layoutDoc.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.layoutDoc.lockedPosition) ? "unlock" : "lock" }); + layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.Document.autoHeight = !this.Document.autoHeight, icon: "plus" }); + layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); + layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); - if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) { - layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" }); - } - if (this.props.Document.type !== DocumentType.COL && this.props.Document.type !== DocumentType.TEMPLATE) { - layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document), icon: "concierge-bell" }); - } else if (this.props.Document.nativeLayout) { + if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) { + layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + } else if (this.props.Document.layoutNative) { layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); @@ -697,19 +508,18 @@ export class DocumentView extends DocComponent(Docu cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); cm.addItem({ - description: "Download document", icon: "download", event: async () => { - let y = JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { + description: "Download document", icon: "download", event: async () => + console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } - })); - console.log(y); - // const a = document.createElement("a"); - // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); - // a.href = url; - // a.download = `DocExport-${this.props.Document[Id]}.zip`; - // a.click(); - } + }))) + // const a = document.createElement("a"); + // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); + // a.href = url; + // a.download = `DocExport-${this.props.Document[Id]}.zip`; + // a.click(); }); + cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); runInAction(() => { if (!ClientUtils.RELEASE) { @@ -754,80 +564,90 @@ export class DocumentView extends DocComponent(Docu }); } - onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; - onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; - isSelected = () => SelectionManager.IsSelected(this); - @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; - @computed get nativeWidth() { return this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } - @computed get contents() { - return (); + // the document containing the view layout information - will be the Document itself unless the Document has + // a layout field. In that case, all layout information comes from there unless overriden by Document + get layoutDoc(): Document { + return Document(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document); } - chromeHeight = () => { - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let templates = Cast(this.layoutDoc.templates, listSpec("string")); - if (!showOverlays && templates instanceof List) { - templates.map(str => { - if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - }); - } - return (showTitle ? 25 : 0) + 1;// bcz: why 8?? - } + // does Document set a layout prop + setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; + // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. + getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); - get layoutDoc() { - // if this document's layout field contains a document (ie, a rendering template), then we will use that - // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. - return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; - } + isSelected = () => SelectionManager.IsSelected(this); + select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; + chromeHeight = () => { + let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; + let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle); + return (showTitle ? 25 : 0) + 1; + } render() { - let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ? - this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) : - StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc); - let foregroundColor = StrCast(this.layoutDoc.color); - var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%"; - var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); - let templates = Cast(this.layoutDoc.templates, listSpec("string")); - if (!showOverlays && templates instanceof List) { - templates.map(str => { - if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - if (!showCaption && str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption"; - }); - } - let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith(" - {StrCast(this.props.Document.search_fields)} + const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; + const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; + const colorSet = this.setsLayoutProp("backgroundColor"); + const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground; + const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ? + this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : + ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); + + const nativeWidth = this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; + const nativeHeight = this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; + const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); + const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); + const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; + const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); + const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding; + const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; + const searchHighlight = (!this.Document.searchFields ? (null) : +
    + {this.Document.searchFields} +
    ); + const captionView = (!showCaption ? (null) : +
    + +
    ); + const titleView = (!showTitle ? (null) : +
    + StrCast(this.Document[showTitle])} + SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true} + />
    ); + const contents = (); return (
    (Docu opacity: this.Document.opacity }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} + onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)} > {!showTitle && !showCaption ? - this.props.Document.search_fields ?
    - {this.contents} - {searchHighlight} -
    : - this.contents : -
    -
    - {this.contents} + this.Document.searchFields ? + (
    + {contents} + {searchHighlight} +
    ) + : + contents + : +
    +
    + {contents}
    - {!showTitle ? (null) : -
    - StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])} - SetValue={(value: string) => ((this.layoutDoc.isTemplate ? this.layoutDoc : Doc.GetProto(this.layoutDoc))[showTitle!] = value) ? true : true} - /> -
    - } - {!showCaption ? (null) : -
    - -
    - } + {titleView} + {captionView} {searchHighlight}
    }
    ); } -} \ No newline at end of file +} + +export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) { + let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc); + if (oldLayoutExt) { + oldLayoutExt.autoHeight = doc.autoHeight; + oldLayoutExt.width = doc.width; + oldLayoutExt.height = doc.height; + oldLayoutExt.nativeWidth = doc.nativeWidth; + oldLayoutExt.nativeHeight = doc.nativeHeight; + oldLayoutExt.ignoreAspect = doc.ignoreAspect; + oldLayoutExt.backgroundLayout = doc.backgroundLayout; + oldLayoutExt.type = doc.type; + oldLayoutExt.layout = doc.layout; + } + + let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc); + if (newLayoutExt) { + doc.autoHeight = newLayoutExt.autoHeight; + doc.width = newLayoutExt.width; + doc.height = newLayoutExt.height; + doc.nativeWidth = newLayoutExt.nativeWidth; + doc.nativeHeight = newLayoutExt.nativeHeight; + doc.ignoreAspect = newLayoutExt.ignoreAspect; + doc.backgroundLayout = newLayoutExt.backgroundLayout; + doc.type = newLayoutExt.type; + doc.layout = await newLayoutExt.layout; + } +} + +Scripting.addGlobal(function toggleDetail(doc: any) { + let native = typeof doc.layout === "string"; + swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom"); +}); \ No newline at end of file diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx index 1f2c88086..6c3db18c4 100644 --- a/src/client/views/nodes/DragBox.tsx +++ b/src/client/views/nodes/DragBox.tsx @@ -45,17 +45,15 @@ export class DragBox extends DocComponent(DragDocu } onDragMove = (e: MouseEvent) => { - if (!e.cancelBubble && !this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { + if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { document.removeEventListener("pointermove", this.onDragMove); document.removeEventListener("pointerup", this.onDragUp); const onDragStart = this.Document.onDragStart; e.stopPropagation(); e.preventDefault(); - let res = onDragStart ? onDragStart.script.run({ this: this.props.Document }) : undefined; - let doc = res !== undefined && res.success ? - res.result as Doc : - Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); - doc && DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc], [undefined]), e.clientX, e.clientY); + let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; + let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); + DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); } e.stopPropagation(); e.preventDefault(); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index d9774303b..49fc2263d 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -30,6 +30,8 @@ export interface FieldViewProps { leaveNativeSize?: boolean; fitToBox?: boolean; ContainingCollectionView: Opt; + ContainingCollectionDoc: Opt; + ruleProvider: Doc | undefined; Document: Doc; DataDoc?: Doc; onClick?: ScriptField; @@ -37,7 +39,7 @@ export interface FieldViewProps { select: (isCtrlPressed: boolean) => void; renderDepth: number; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index d7ac7a9c5..0d7277cbe 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -4,7 +4,6 @@ width: 100%; height: 100%; min-height: 100%; - font-family: $serif; } .ProseMirror:focus { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 8f0f142c4..cb9fecfc5 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -41,7 +41,7 @@ import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import * as _ from "lodash"; import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; import { inputRules } from 'prosemirror-inputrules'; -import { select } from 'async'; +import { DocumentButtonBar } from '../DocumentButtonBar'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -81,15 +81,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _linkClicked = ""; private _nodeClicked: any; private _undoTyping?: UndoManager.Batch; - private _reactionDisposer: Opt; private _searchReactionDisposer?: Lambda; + private _reactionDisposer: Opt; private _textReactionDisposer: Opt; private _heightReactionDisposer: Opt; + private _rulesReactionDisposer: Opt; private _proxyReactionDisposer: Opt; private _pullReactionDisposer: Opt; private _pushReactionDisposer: Opt; private dropDisposer?: DragManager.DragDropDisposer; + @observable private _fontSize = 13; + @observable private _fontFamily = "Arial"; + @observable private _fontAlign = ""; @observable private _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; public static SelectOnLoad = ""; @@ -121,14 +125,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @undoBatch public setFontColor(color: string) { - this._editorView!.state.storedMarks; - if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false; - if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) { + let view = this._editorView!; + if (view.state.selection.from === view.state.selection.to) return false; + if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) { this.props.Document.color = color; } - let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color }); - this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from, - this._editorView!.state.selection.to, colorMark)); + let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); + view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark)); return true; } @@ -142,10 +145,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); } - - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } // this should be internal to prosemirror, but is needed // here to make sure that footnote view nodes in the overlay editor @@ -170,6 +172,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + linkOnDeselect: Map = new Map(); + + doLinkOnDeselect() { + Array.from(this.linkOnDeselect.entries()).map(entry => { + let key = entry[0]; + let value = entry[1]; + let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + DocServer.GetRefField(value).then(doc => { + DocServer.GetRefField(id).then(linkDoc => { + this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); + DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument); + if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } + else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true); + }); + }); + }); + this.linkOnDeselect.clear(); + } + dispatchTransaction = (tx: Transaction) => { if (this._editorView) { let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); @@ -184,15 +205,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (split.length > 1 && split[1]) { let key = split[0]; let value = split[split.length - 1]; + this.linkOnDeselect.set(key, value); let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - DocServer.GetRefField(value).then(doc => { - DocServer.GetRefField(id).then(linkDoc => { - this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); - if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } - else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id); - }); - }); const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); let offset = (tx.selection.to === range!.end - 1 ? -1 : 0); @@ -256,12 +271,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } }); - // const fieldkey = 'search_string'; - // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { - // this.props.Document[fieldkey] = undefined; - // } - // else this.props.Document.proto![fieldkey] = undefined; - // } } } setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { @@ -292,16 +301,29 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.stopPropagation(); } else if (de.data instanceof DragManager.DocumentDragData) { const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; - if (draggedDoc && draggedDoc.type === DocumentType.TEXT && StrCast(draggedDoc.layout) !== "") { - this.props.Document.layout = draggedDoc; - draggedDoc.isTemplate = true; + if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + if (de.mods === "AltKey") { + if (draggedDoc.data instanceof RichTextField) { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data); + e.stopPropagation(); + } + } else { + draggedDoc.isTemplate = true; + if (typeof (draggedDoc.layout) === "string") { + let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc); + layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`); + this.props.Document.layout = layoutDelegateToOverrideFieldKey; + } else { + this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc; + } + } e.stopPropagation(); } } } recordKeyHandler = (e: KeyboardEvent) => { - if (this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) { + if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) { if (e.key === "R" && e.altKey) { e.stopPropagation(); e.preventDefault(); @@ -380,6 +402,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } componentDidMount() { + if (!this.props.isOverlay) { this._proxyReactionDisposer = reaction(() => this.props.isSelected(), () => { @@ -410,8 +433,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._pullReactionDisposer = reaction( () => this.props.Document[Pulls], () => { - if (!DocumentDecorations.hasPulledHack) { - DocumentDecorations.hasPulledHack = true; + if (!DocumentButtonBar.hasPulledHack) { + DocumentButtonBar.hasPulledHack = true; let unchanged = this.dataDoc.unchanged; this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); } @@ -421,8 +444,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._pushReactionDisposer = reaction( () => this.props.Document[Pushes], () => { - if (!DocumentDecorations.hasPushedHack) { - DocumentDecorations.hasPushedHack = true; + if (!DocumentButtonBar.hasPushedHack) { + DocumentButtonBar.hasPushedHack = true; this.pushToGoogleDoc(); } } @@ -462,6 +485,39 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.unhighlightSearchTerms(); } }, { fireImmediately: true }); + + + this._rulesReactionDisposer = reaction(() => { + let ruleProvider = this.props.ruleProvider; + let heading = NumCast(this.props.Document.heading); + if (ruleProvider instanceof Doc) { + return { + align: StrCast(ruleProvider["ruleAlign_" + heading], ""), + font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"), + size: NumCast(ruleProvider["ruleSize_" + heading], 13) + }; + } + return undefined; + }, + action((rules: any) => { + this._fontFamily = rules ? rules.font : "Arial"; + this._fontSize = rules ? rules.size : 13; + rules && setTimeout(() => { + const view = this._editorView!; + if (this._proseRef) { + let n = new NodeSelection(view.state.doc.resolve(0)); + if (this._editorView!.state.doc.textContent === "") { + view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))). + replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true)); + } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) { + view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align })); + } + this.tryUpdateHeight(); + } + }, 0); + }), { fireImmediately: true } + ); + setTimeout(() => this.tryUpdateHeight(), 0); } @@ -481,7 +537,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; - DocumentDecorations.Instance.startPushOutcome(pushSuccess); + DocumentButtonBar.Instance.startPushOutcome(pushSuccess); } }; let undo = () => { @@ -529,7 +585,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } else { delete dataDoc[GoogleRef]; } - DocumentDecorations.Instance.startPullOutcome(pullSuccess); + DocumentButtonBar.Instance.startPullOutcome(pullSuccess); } checkState = (exportState: Opt, dataDoc: Doc) => { @@ -538,7 +594,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let equalTitles = dataDoc.title === exportState.title; let unchanged = equalContent && equalTitles; dataDoc.unchanged = unchanged; - DocumentDecorations.Instance.setPullState(unchanged); + DocumentButtonBar.Instance.setPullState(unchanged); } } @@ -585,7 +641,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let annotations = DocListCast(region.annotations); annotations.forEach(anno => anno.target = this.props.Document); - let fieldExtDoc = Doc.resolvedFieldDataDoc(doc, "data", "true"); + let fieldExtDoc = Doc.fieldExtensionDoc(doc, "data"); let targetAnnotations = DocListCast(fieldExtDoc.annotations); if (targetAnnotations) { targetAnnotations.push(region); @@ -666,27 +722,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe else if (this.props.isOverlay) this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; - let heading = this.props.Document.heading; - if (heading && selectOnLoad) { - PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => { - if (ruleProvider) { - let align = StrCast(ruleProvider["ruleAlign_" + heading]); - let font = StrCast(ruleProvider["ruleFont_" + heading]); - let size = NumCast(ruleProvider["ruleSize_" + heading]); - if (align) { - let tr = this._editorView!.state.tr; - tr = tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(2))). - replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: align }), true). - setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(0))); - this._editorView!.dispatch(tr); - } - let sm = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; - size && (sm = [...sm, schema.marks.pFontSize.create({ fontSize: size })]); - font && (sm = [...sm, this.getFont(font)]); - this._editorView!.dispatch(this._editorView!.state.tr.setStoredMarks(sm)); - } - }); - } } getFont(font: string) { switch (font) { @@ -702,6 +737,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } componentWillUnmount() { + this._rulesReactionDisposer && this._rulesReactionDisposer(); this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._textReactionDisposer && this._textReactionDisposer(); @@ -857,6 +893,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._undoTyping.end(); this._undoTyping = undefined; } + this.doLinkOnDeselect(); } onKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Escape") { @@ -866,7 +903,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })); + this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); @@ -877,7 +914,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe tryUpdateHeight() { const ChromeHeight = this.props.ChromeHeight; let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.props.isOverlay && this.props.Document.autoHeight && sh !== 0) { + if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) { let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); @@ -885,12 +922,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - render() { let style = this.props.isOverlay ? "scroll" : "hidden"; let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; - let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground || - (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all"; + let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground + ? "none" : "all"; Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); return (
    this._entered = true)} onPointerLeave={action(() => this._entered = false)} > -
    +
    ); } diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 7e78ec684..63a504d1a 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -12,6 +12,8 @@ import { IconField } from "../../../new_fields/IconField"; import { ContextMenu } from "../ContextMenu"; import Measure from "react-measure"; import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss"; +import { Scripting } from "../../util/Scripting"; +import { ComputedField } from "../../../new_fields/ScriptField"; library.add(faCaretUp); @@ -27,6 +29,25 @@ export class IconBox extends React.Component { @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "

    Error loading icon data

    "; } @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } + public static summaryTitleScript(inputDoc: Doc) { + const sumDoc = Cast(inputDoc.summaryDoc, Doc) as Doc; + if (sumDoc && StrCast(sumDoc.title).startsWith("-")) { + return sumDoc.title + ".expanded"; + } + return "???"; + } + public static titleScript(inputDoc: Doc) { + const maxDoc = DocListCast(inputDoc.maximizedDocs); + if (maxDoc.length === 1) { + return maxDoc[0].title + ".icon"; + } + return maxDoc.length > 1 ? "-multiple-.icon" : "???"; + } + + public static AutomaticTitle(doc: Doc) { + Doc.GetProto(doc).title = ComputedField.MakeFunction('iconTitle(this);'); + } + public static DocumentIcon(layout: string) { let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : layout.indexOf("ImageBox") !== -1 ? faImage : @@ -38,35 +59,20 @@ export class IconBox extends React.Component { } setLabelField = (): void => { - this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); - } - setUseOwnTitleField = (): void => { - this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); + this.props.Document.hideLabel = !this.props.Document.hideLabel; } specificContextMenu = (): void => { - ContextMenu.Instance.addItem({ - description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", - event: this.setLabelField, - icon: "tag" - }); - let maxDocs = DocListCast(this.props.Document.maximizedDocs); - if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) { - ContextMenu.Instance.addItem({ - description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label", - event: this.setUseOwnTitleField, - icon: "text-height" - }); + let cm = ContextMenu.Instance; + cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" }); + if (!this.props.Document.hideLabel) { + cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" }); } } @observable _panelWidth: number = 0; @observable _panelHeight: number = 0; render() { - let labelField = StrCast(this.props.Document.labelField); - let hideLabel = BoolCast(this.props.Document.hideLabel); - let maxDocs = DocListCast(this.props.Document.maximizedDocs); - let firstDoc = maxDocs.length ? maxDocs[0] : undefined; - let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle) ? firstDoc[labelField] : this.props.Document.title); + let label = this.props.Document.hideLabel ? "" : this.props.Document.title; return (
    {this.minimizedIcon} @@ -82,4 +88,6 @@ export class IconBox extends React.Component {
    ); } -} \ No newline at end of file +} +Scripting.addGlobal(function iconTitle(doc: any) { return IconBox.titleScript(doc); }); +Scripting.addGlobal(function summaryTitle(doc: any) { return IconBox.summaryTitleScript(doc); }); \ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 649d2d056..1645c4ffd 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -17,13 +17,12 @@ import { Utils } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; -import { CompileScript } from '../../util/Scripting'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { InkingControl } from '../InkingControl'; -import { positionSchema } from './DocumentView'; +import { documentSchema } from './DocumentView'; import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; @@ -50,8 +49,8 @@ declare class MediaRecorder { constructor(e: any); } -type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>; -const ImageDocument = makeInterface(pageSchema, positionSchema); +type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; +const ImageDocument = makeInterface(pageSchema, documentSchema); @observer export class ImageBox extends DocComponent(ImageDocument) { @@ -65,7 +64,9 @@ export class ImageBox extends DocComponent(ImageD private dropDisposer?: DragManager.DragDropDisposer; @observable private hoverActive = false; - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { @@ -81,32 +82,18 @@ export class ImageBox extends DocComponent(ImageD console.log("IMPLEMENT ME PLEASE"); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); } - @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { - de.data.droppedDocuments.forEach(action((drop: Doc) => { - if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); - e.stopPropagation(); - } else if (de.mods === "MetaKey") { - if (this.extensionDoc !== this.dataDoc) { - let layout = StrCast(drop.backgroundLayout); - if (layout.indexOf(ImageBox.name) !== -1) { - let imgData = this.extensionDoc.Alternates; - if (!imgData) { - Doc.GetProto(this.extensionDoc).Alternates = new List([]); - } - let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]); - imgList && imgList.push(drop); - e.stopPropagation(); - } - } - } + if (de.mods === "AltKey" && de.data.draggedDocuments.length && de.data.draggedDocuments[0].data instanceof ImageField) { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.data.draggedDocuments[0].data.url); + e.stopPropagation(); + } + de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => { + Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop); + e.stopPropagation(); })); - // de.data.removeDocument() bcz: need to implement } } @@ -243,9 +230,7 @@ export class ImageBox extends DocComponent(ImageD results.tags.map((tag: Tag) => { tagsList.push(tag.name); let sanitized = tag.name.replace(" ", "_"); - let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`; - let computed = CompileScript(script, { params: { this: "Doc" } }); - computed.compiled && (tagDoc[sanitized] = new ComputedField(computed)); + tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); this.extensionDoc.generatedTags = tagsList; tagDoc.title = "Generated Tags Doc"; @@ -261,13 +246,8 @@ export class ImageBox extends DocComponent(ImageD onDotDown(index: number) { this.Document.curPage = index; } - - @computed get fieldExtensionDoc() { - return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); - } - @computed private get url() { - let data = Cast(Doc.GetProto(this.props.Document).data, ImageField); + let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField); return data ? data.url.href : undefined; } @@ -407,7 +387,6 @@ export class ImageBox extends DocComponent(ImageD // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); // let w = bptX - sptX; - let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx let nativeWidth = FieldValue(this.Document.nativeWidth, pw); let nativeHeight = FieldValue(this.Document.nativeHeight, 0); let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; @@ -433,13 +412,13 @@ export class ImageBox extends DocComponent(ImageD if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); return ( -
    this.hoverActive = true)} onPointerLeave={action(() => this.hoverActive = false)} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
    - { } else if (type === "script") { field = new ScriptField(script); } else { - let res = script.run({ this: target }); + let res = script.run({ this: target }, console.log); if (!res.success) return false; field = res.result; } @@ -124,7 +124,7 @@ export class KeyValueBox extends React.Component { let i = 0; const self = this; for (let key of Object.keys(ids).slice().sort()) { - rows.push( { if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); @@ -198,7 +198,7 @@ export class KeyValueBox extends React.Component { return; } let previousViewType = fieldTemplate.viewType; - Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc)); + Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc)); previousViewType && (fieldTemplate.viewType = previousViewType); Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index a27dbd83d..1fed4c8bb 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { Doc, Field } from '../../../new_fields/Doc'; +import { Doc, Field, Opt } from '../../../new_fields/Doc'; import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; @@ -22,6 +22,7 @@ export interface KeyValuePairProps { keyName: string; doc: Doc; keyWidth: number; + addDocTab: (doc: Doc, data: Opt, where: string) => boolean; } @observer export class KeyValuePair extends React.Component { @@ -45,7 +46,7 @@ export class KeyValuePair extends React.Component { if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } } @@ -55,6 +56,8 @@ export class KeyValuePair extends React.Component { Document: this.props.doc, DataDoc: this.props.doc, ContainingCollectionView: undefined, + ContainingCollectionDoc: undefined, + ruleProvider: undefined, fieldKey: this.props.keyName, fieldExt: "", isSelected: returnFalse, @@ -66,7 +69,7 @@ export class KeyValuePair extends React.Component { focus: emptyFunction, PanelWidth: returnZero, PanelHeight: returnZero, - addDocTab: returnZero, + addDocTab: returnFalse, pinToPres: returnZero, ContentScaling: returnOne }; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index df35b603c..764051d62 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -4,40 +4,28 @@ import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import 'react-image-lightbox/style.css'; -import { Doc, WidthSym, Opt } from "../../../new_fields/Doc"; +import { Doc, Opt, WidthSym } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast } from "../../../new_fields/Types"; +import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; +import { Cast, NumCast } from "../../../new_fields/Types"; import { PdfField } from "../../../new_fields/URLField"; import { KeyCodes } from '../../northstar/utils/KeyCodes'; -import { CompileScript } from '../../util/Scripting'; +import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; import { PDFViewer } from "../pdf/PDFViewer"; -import { positionSchema } from "./DocumentView"; +import { documentSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); -type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; -const PdfDocument = makeInterface(positionSchema, pageSchema); -export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === KeyCodes.BACKSPACE) e.stopPropagation(); }; +type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; +const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @observer export class PDFBox extends DocComponent(PdfDocument) { public static LayoutString() { return FieldView.LayoutString(PDFBox); } - - @observable private _flyout: boolean = false; - @observable private _alt = false; - @observable private _pdf: Opt; - - @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; } - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - - - @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); } - private _mainCont: React.RefObject = React.createRef(); private _reactionDisposer?: IReactionDisposer; private _keyValue: string = ""; @@ -47,16 +35,26 @@ export class PDFBox extends DocComponent(PdfDocumen private _valueRef: React.RefObject = React.createRef(); private _scriptRef: React.RefObject = React.createRef(); + @observable private _flyout: boolean = false; + @observable private _alt = false; + @observable private _pdf: Opt; + + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + componentDidMount() { this.props.setPdfBox && this.props.setPdfBox(this); + this.props.Document.curPage = ComputedField.MakeFunction("Math.floor(Number(this.panY) / Number(this.nativeHeight) + 1)"); + const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); if (pdfUrl instanceof PdfField) { Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); } this._reactionDisposer = reaction( - () => this.props.Document.panY, - () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.panY), behavior: "auto" }) + () => this.Document.panY, + () => this._mainCont.current && this._mainCont.current.scrollTo({ top: this.Document.panY || 0, behavior: "auto" }) ); } @@ -65,24 +63,22 @@ export class PDFBox extends DocComponent(PdfDocumen } public GetPage() { - return Math.floor(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1; + return Math.floor((this.Document.panY || 0) / (this.Document.nativeHeight || 0)) + 1; } @action public BackPage() { - let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1; + let cp = Math.ceil((this.Document.panY || 0) / (this.Document.nativeHeight || 0)) + 1; cp = cp - 1; if (cp > 0) { - this.props.Document.curPage = cp; - this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); + this.Document.panY = (cp - 1) * (this.Document.nativeHeight || 0); } } @action - public GotoPage(p: number) { + public GotoPage = (p: number) => { if (p > 0 && p <= NumCast(this.dataDoc.numPages)) { - this.props.Document.curPage = p; - this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight); + this.Document.panY = (p - 1) * (this.Document.nativeHeight || 0); } } @@ -90,23 +86,20 @@ export class PDFBox extends DocComponent(PdfDocumen public ForwardPage() { let cp = this.GetPage() + 1; if (cp <= NumCast(this.dataDoc.numPages)) { - this.props.Document.curPage = cp; - this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); + this.Document.panY = (cp - 1) * (this.Document.nativeHeight || 0); } } @action setPanY = (y: number) => { - this.containingCollectionDocument && (this.containingCollectionDocument.panY = y); + this.Document.panY = y; } @action private applyFilter = () => { - let scriptText = this._scriptValue.length > 0 ? this._scriptValue : - this._keyValue.length > 0 && this._valueValue.length > 0 ? - `return this.${this._keyValue} === ${this._valueValue}` : "return true"; - let script = CompileScript(scriptText, { params: { this: Doc.name } }); - script.compiled && (this.props.Document.filterScript = new ScriptField(script)); + let scriptText = this._scriptValue ? this._scriptValue : + this._keyValue && this._valueValue ? `this.${this._keyValue} === ${this._valueValue}` : "true"; + this.props.Document.filterScript = ScriptField.MakeFunction(scriptText); } scrollTo = (y: number) => { @@ -114,8 +107,7 @@ export class PDFBox extends DocComponent(PdfDocumen } private resetFilters = () => { - this._keyValue = this._valueValue = ""; - this._scriptValue = "return true"; + this._keyValue = this._valueValue = this._scriptValue = ""; this._keyRef.current && (this._keyRef.current.value = ""); this._valueRef.current && (this._valueRef.current.value = ""); this._scriptRef.current && (this._scriptRef.current.value = ""); @@ -129,7 +121,7 @@ export class PDFBox extends DocComponent(PdfDocumen return !this.props.active() ? (null) : (
    e.stopPropagation()}> -
    - - - - -
    - -
    -
    - -
    -
    - - ); - } -} \ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 05904e1e7..2b6a86aed 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -291,7 +291,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (de.data.urlField && link) { let url: string = de.data.urlField.url.href; let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image; - node = model.create({ src: url, docid: link[Id] }) + node = model.create({ src: url, docid: link[Id] }); } else { node = schema.nodes.dashDoc.create({ width: target[WidthSym](), height: target[HeightSym](), @@ -798,7 +798,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._editorView!.focus(); } } - } + }; } onPointerUp = (e: React.PointerEvent): void => { @@ -807,9 +807,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.stopPropagation(); } } - + static InputBoxOverlay: any = null; @action onFocused = (e: React.FocusEvent): void => { + FormattedTextBox.InputBoxOverlay = this; document.removeEventListener("keypress", this.recordKeyHandler); document.addEventListener("keypress", this.recordKeyHandler); this.tryUpdateHeight(); diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index 0c6df74f0..cc326eb93 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -2,6 +2,5 @@ pointer-events: all; user-select: none; position: absolute; - background-color: red; - opacity: 0.1; + background-color: rgba(146, 245, 95, 0.467); } \ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 134e757d1..4bb166ffe 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -122,9 +122,9 @@ class RegionAnnotation extends React.Component { left: this.props.x, width: this.props.width, height: this.props.height, - transition: "opacity 0.5s", opacity: this._brushed ? 0.5 : undefined, - backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.color) + backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor), + transition: "opacity 0.5s", }} />); } } \ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index e62542014..1e3320069 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -4,7 +4,7 @@ import { observable, action, } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { emptyFunction, returnFalse } from "../../../Utils"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, Opt } from "../../../new_fields/Doc"; @observer export default class PDFMenu extends React.Component { @@ -31,7 +31,7 @@ export default class PDFMenu extends React.Component { @observable public Pinned: boolean = false; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; - public Highlight: (color: string) => void = emptyFunction; + public Highlight: (color: string) => Opt = (color: string) => undefined; public Delete: () => void = emptyFunction; public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; @@ -155,12 +155,8 @@ export default class PDFMenu extends React.Component { @action highlightClicked = (e: React.MouseEvent) => { - if (!this.Pinned) { - this.Highlight("#f4f442"); - } - else { + if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { this.Highlighting = !this.Highlighting; - this.Highlight("#f4f442"); } } diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index a71e4f81e..c77cee792 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -16,6 +16,7 @@ mix-blend-mode: multiply; opacity: 0.9; } + .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation() .textLayer .highlight { background-color: yellow; } @@ -51,10 +52,9 @@ pointer-events: none; mix-blend-mode: multiply; - .pdfPage-annotationBox { + .pdfViewer-annotationBox { position: absolute; - background-color: red; - opacity: 0.1; + background-color: rgba(245, 230, 95, 0.616); } } .pdfViewer-waiting { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 1b76ddbdc..4516e9904 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -75,8 +75,9 @@ export class PDFViewer extends React.Component { @observable private _showWaiting = true; @observable private _showCover = false; @observable private _zoomed = 1; + @observable private _scrollTop = 0; - public pdfViewer: any; + private _pdfViewer: any; private _retries = 0; // number of times tried to create the PDF viewer private _isChildActive = false; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); @@ -149,15 +150,16 @@ export class PDFViewer extends React.Component { copy = (e: ClipboardEvent) => { if (this.props.active() && e.clipboardData) { - e.clipboardData.setData("text/plain", this._selectionText); - e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument("#0390fc")[Id]); + let annoDoc = this.makeAnnotationDocument("#0390fc"); + if (annoDoc) { + e.clipboardData.setData("text/plain", this._selectionText); + e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); + e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]); + } e.preventDefault(); } } - setSelectionText = (text: string) => this._selectionText = text; - @action initialLoad = async () => { if (this._pageSizes.length === 0) { @@ -215,7 +217,7 @@ export class PDFViewer extends React.Component { document.removeEventListener("copy", this.copy); document.addEventListener("copy", this.copy); document.addEventListener("pagesinit", action(() => { - this.pdfViewer.currentScaleValue = this._zoomed = 1; + this._pdfViewer.currentScaleValue = this._zoomed = 1; this.gotoPage(NumCast(this.props.Document.curPage, 1)); })); document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false)); @@ -223,36 +225,35 @@ export class PDFViewer extends React.Component { let pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, }); - this.pdfViewer = new PDFJSViewer.PDFViewer({ + this._pdfViewer = new PDFJSViewer.PDFViewer({ container: this._mainCont.current, viewer: this._viewer.current, linkService: pdfLinkService, findController: pdfFindController, renderer: "canvas", }); - pdfLinkService.setViewer(this.pdfViewer); + pdfLinkService.setViewer(this._pdfViewer); pdfLinkService.setDocument(this.props.pdf, null); - this.pdfViewer.setDocument(this.props.pdf); + this._pdfViewer.setDocument(this.props.pdf); } @action - makeAnnotationDocument = (color: string): Doc => { + makeAnnotationDocument = (color: string): Opt => { + if (this._savedAnnotations.size() === 0) return undefined; let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); let annoDocs: Doc[] = []; let minY = Number.MAX_VALUE; - if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1) { + if ((this._savedAnnotations.values()[0][0] as any).marqueeing) { let anno = this._savedAnnotations.values()[0][0]; - let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) }); + let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + StrCast(this.props.Document.title) }); if (anno.style.left) annoDoc.x = parseInt(anno.style.left); if (anno.style.top) annoDoc.y = parseInt(anno.style.top); if (anno.style.height) annoDoc.height = parseInt(anno.style.height); if (anno.style.width) annoDoc.width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; - annoDoc.color = color; - annoDoc.type = AnnotationTypes.Region; - annoDocs.push(annoDoc); annoDoc.isButton = true; + annoDocs.push(annoDoc); anno.remove(); mainAnnoDoc = annoDoc; mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); @@ -265,8 +266,7 @@ export class PDFViewer extends React.Component { if (anno.style.height) annoDoc.height = parseInt(anno.style.height); if (anno.style.width) annoDoc.width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; - annoDoc.color = color; - annoDoc.type = AnnotationTypes.Region; + annoDoc.backgroundColor = color; annoDocs.push(annoDoc); anno.remove(); (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); @@ -307,7 +307,7 @@ export class PDFViewer extends React.Component { @action gotoPage = (p: number) => { - this.pdfViewer && this.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); } @action @@ -317,25 +317,11 @@ export class PDFViewer extends React.Component { Doc.linkFollowHighlight(scrollToAnnotation); } - sendAnnotations = (page: number) => { - return this._savedAnnotations.getValue(page); - } - - receiveAnnotations = (annotations: HTMLDivElement[], page: number) => { - if (page === -1) { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations)); - } - else { - this._savedAnnotations.setValue(page, annotations); - } - } - @observable scrollTop = 0; @action onScroll = (e: React.UIEvent) => { - this.scrollTop = this._mainCont.current!.scrollTop; - this.pdfViewer && (this.props.Document.curPage = this.pdfViewer.currentPageNumber); + this._scrollTop = this._mainCont.current!.scrollTop; + this._pdfViewer && (this.props.Document.curPage = this._pdfViewer.currentPageNumber); } // get the page index that the vertical offset passed in is on @@ -355,6 +341,8 @@ export class PDFViewer extends React.Component { div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString(); } this._annotationLayer.current.append(div); + div.style.backgroundColor = "yellow"; + div.style.opacity = "0.5"; let savedPage = this._savedAnnotations.getValue(page); if (savedPage) { savedPage.push(div); @@ -371,8 +359,8 @@ export class PDFViewer extends React.Component { if (!searchString) { fwd ? this.nextAnnotation() : this.prevAnnotation(); } - else if (this.pdfViewer._pageViewsReady) { - this.pdfViewer.findController.executeCommand('findagain', { + else if (this._pdfViewer._pageViewsReady) { + this._pdfViewer.findController.executeCommand('findagain', { caseSensitive: false, findPrevious: !fwd, highlightAll: true, @@ -382,7 +370,7 @@ export class PDFViewer extends React.Component { } else if (this._mainCont.current) { let executeFind = () => { - this.pdfViewer.findController.executeCommand('find', { + this._pdfViewer.findController.executeCommand('find', { caseSensitive: false, findPrevious: !fwd, highlightAll: true, @@ -406,30 +394,24 @@ export class PDFViewer extends React.Component { } this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active()) { + // clear out old marquees and initialize menu for new selection PDFMenu.Instance.StartDrag = this.startDrag; PDFMenu.Instance.Highlight = this.highlight; PDFMenu.Instance.Snippet = this.createSnippet; PDFMenu.Instance.Status = "pdf"; PDFMenu.Instance.fadeOut(true); + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); if (e.target && (e.target as any).parentElement.className === "textLayer") { - if (!e.ctrlKey) { - this.receiveAnnotations([], -1); - } + // start selecting text if mouse down on textLayer spans } - else { + else if (this._mainCont.current) { // set marquee x and y positions to the spatially transformed position - if (this._mainCont.current) { - let boundingRect = this._mainCont.current.getBoundingClientRect(); - this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); - this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; - } + let boundingRect = this._mainCont.current.getBoundingClientRect(); + this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); + this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; + this._marqueeHeight = this._marqueeWidth = 0; this._marqueeing = true; - let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox"); - if (marquees && marquees.length) { // make a copy of the marquee - let marquee = marquees[0] as HTMLDivElement; - marquee.style.opacity = "0.2"; - } - this.receiveAnnotations([], -1); } document.removeEventListener("pointermove", this.onSelectMove); document.addEventListener("pointermove", this.onSelectMove); @@ -464,13 +446,13 @@ export class PDFViewer extends React.Component { let clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { let rect = clientRects.item(i); - if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) { + if (rect) { let scaleY = this._mainCont.current.offsetHeight / boundingRect.height; let scaleX = this._mainCont.current.offsetWidth / boundingRect.width; if (rect.width !== this._mainCont.current.clientWidth && - (i == 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) { + (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) { let annoBox = document.createElement("div"); - annoBox.className = "pdfPage-annotationBox"; + annoBox.className = "pdfViewer-annotationBox"; // transforms the positions from screen onto the pdf div annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString(); annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString(); @@ -481,8 +463,7 @@ export class PDFViewer extends React.Component { } } } - let text = selRange.cloneContents().textContent; - text && this.setSelectionText(text); + this._selectionText = selRange.cloneContents().textContent || ""; // clear selection if (sel.empty) { // Chrome @@ -494,22 +475,22 @@ export class PDFViewer extends React.Component { @action onSelectEnd = (e: PointerEvent): void => { + this._savedAnnotations.clear(); if (this._marqueeing) { if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox"); - if (marquees && marquees.length) { // make a copy of the marquee + if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation. + let style = (marquees[0] as HTMLDivElement).style; let copy = document.createElement("div"); - let marquee = marquees[0] as HTMLDivElement; - let style = marquee.style; copy.style.left = style.left; copy.style.top = style.top; copy.style.width = style.width; copy.style.height = style.height; copy.style.border = style.border; copy.style.opacity = style.opacity; - copy.className = "pdfPage-annotationBox"; + (copy as any).marqueeing = true; + copy.className = "pdfViewer-annotationBox"; this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY)); - marquee.style.opacity = "0"; } if (!e.ctrlKey) { @@ -518,8 +499,7 @@ export class PDFViewer extends React.Component { } PDFMenu.Instance.jumpTo(e.clientX, e.clientY); } - - this._marqueeHeight = this._marqueeWidth = 0; + this._marqueeing = false; } else { let sel = window.getSelection(); @@ -531,7 +511,7 @@ export class PDFViewer extends React.Component { } if (PDFMenu.Instance.Highlighting) { - this.highlight("goldenrod"); + this.highlight("rgba(245, 230, 95, 0.616)"); // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up } else { PDFMenu.Instance.StartDrag = this.startDrag; @@ -545,7 +525,7 @@ export class PDFViewer extends React.Component { highlight = (color: string) => { // creates annotation documents for current highlights let annotationDoc = this.makeAnnotationDocument(color); - Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc); + annotationDoc && Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc); return annotationDoc; } @@ -558,16 +538,18 @@ export class PDFViewer extends React.Component { e.preventDefault(); e.stopPropagation(); let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title }); - let annotationDoc = this.highlight("red"); - let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); - DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { - handlers: { - dragComplete: () => !(dragData as any).linkedToDoc && - DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF") - - }, - hideSource: false - }); + const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); + if (annotationDoc) { + let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); + DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { + handlers: { + dragComplete: () => !(dragData as any).linkedToDoc && + DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF") + + }, + hideSource: false + }); + } } createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { @@ -609,7 +591,7 @@ export class PDFViewer extends React.Component { return true; } scrollXf = () => { - return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this.scrollTop) : this.props.ScreenToLocalTransform(); + return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform(); } setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => { this._setPreviewCursor = func; @@ -647,9 +629,9 @@ export class PDFViewer extends React.Component { onZoomWheel = (e: React.WheelEvent) => { e.stopPropagation(); if (e.ctrlKey) { - let curScale = Number(this.pdfViewer.currentScaleValue); - this.pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000)); - this._zoomed = Number(this.pdfViewer.currentScaleValue); + let curScale = Number(this._pdfViewer.currentScaleValue); + this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000)); + this._zoomed = Number(this._pdfViewer.currentScaleValue); } } @@ -657,29 +639,7 @@ export class PDFViewer extends React.Component { return
    {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => )} -
    ; - } - @computed get pdfViewerDiv() { - return
    ; - } - @computed get standinViews() { - return <> - {this._showCover ? this.getCoverImage() : (null)} - {this._showWaiting ? : (null)} - ; - } - marqueeWidth = () => this._marqueeWidth; - marqueeHeight = () => this._marqueeHeight; - marqueeX = () => this._marqueeX; - marqueeY = () => this._marqueeY; - marqueeing = () => this._marqueeing; - visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; - render() { - return (
    - {this.pdfViewerDiv} - - {this.annotationLayer} -
    +
    NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))} @@ -702,7 +662,29 @@ export class PDFViewer extends React.Component { chromeCollapsed={true}>
    +
    ; + } + @computed get pdfViewerDiv() { + return
    ; + } + @computed get standinViews() { + return <> + {this._showCover ? this.getCoverImage() : (null)} + {this._showWaiting ? : (null)} + ; + } + marqueeWidth = () => this._marqueeWidth; + marqueeHeight = () => this._marqueeHeight; + marqueeX = () => this._marqueeX; + marqueeY = () => this._marqueeY; + marqueeing = () => this._marqueeing; + visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; + render() { + return (
    + {this.pdfViewerDiv} + {this.annotationLayer} {this.standinViews} +
    ); } } @@ -722,11 +704,9 @@ class PdfViewerMarquee extends React.Component { style={{ left: `${this.props.x()}px`, top: `${this.props.y()}px`, width: `${this.props.width()}px`, height: `${this.props.height()}px`, - border: `${this.props.width() === 0 ? "" : "2px dashed black"}` + border: `${this.props.width() === 0 ? "" : "2px dashed black"}`, + opacity: 0.2 }}>
    ; } -} - - -export enum AnnotationTypes { Region } +} \ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 6acc6e1ca..7e37eba84 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -685,7 +685,7 @@ export namespace Doc { document.removeEventListener("pointerdown", linkFollowUnhighlight); document.addEventListener("pointerdown", linkFollowUnhighlight); let x = dt = Date.now(); - window.setTimeout(() => dt == x && linkFollowUnhighlight(), 5000); + window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000); } export class HighlightBrush { diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index b2509a4f1..0fbfbf2f3 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -112,7 +112,7 @@ export class CurrentUserUtils { if (sidebar) { sidebar.backgroundColor = "lightgrey"; } - }) + }); if (doc.overlays === undefined) { const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" }); -- cgit v1.2.3-70-g09d2 From c17fba4c07a1cc0e40d08228b4cdc4182615496b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 5 Oct 2019 15:31:25 -0400 Subject: beginning external file download --- src/client/util/Import & Export/ImageUtils.ts | 33 +++++++++++++++++++++++++-- src/server/RouteStore.ts | 1 + src/server/index.ts | 10 ++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) (limited to 'src/server') diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 33ca55aa9..bf482aea8 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -1,9 +1,11 @@ -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; import { ImageField } from "../../../new_fields/URLField"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { RouteStore } from "../../../server/RouteStore"; import { Docs } from "../../documents/Documents"; import { Identified } from "../../Network"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { Utils } from "../../../Utils"; export namespace ImageUtils { @@ -19,4 +21,31 @@ export namespace ImageUtils { return data !== undefined; }; + export type Hierarchy = { [id: string]: string | Hierarchy }; + + export const ExportHierarchyToFileSystem = async (doc: Doc): Promise => { + const hierarchy: Hierarchy = {}; + await HierarchyTraverserRecursive(doc, hierarchy); + const a = document.createElement("a"); + a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${JSON.stringify(hierarchy)}`); + a.download = `Full Export of ${StrCast(doc.title)}`; + a.click(); + }; + + const HierarchyTraverserRecursive = async (collection: Doc, hierarchy: Hierarchy) => { + const children = await DocListCastAsync(collection.data); + if (children) { + const local: Hierarchy = {}; + hierarchy[collection[Id]] = local; + for (const child of children) { + let imageData: Opt; + if (imageData = Cast(child.data, ImageField)) { + local[child[Id]] = imageData.url.href; + } else { + await HierarchyTraverserRecursive(child, local); + } + } + } + }; + } \ No newline at end of file diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 1e1dd6300..8a9a6baae 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -14,6 +14,7 @@ export enum RouteStore { dataUriToImage = "/uploadURI", images = "/images", inspectImage = "/inspectImage", + imageHierarchyExport = "/imageHierarchyExport", // USER AND WORKSPACES getCurrUser = "/getCurrentUser", diff --git a/src/server/index.ts b/src/server/index.ts index 1ca5d6e11..f9ca3de56 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -320,6 +320,16 @@ app.get("/serializeDoc/:docId", async (req, res) => { res.send({ docs, files: Array.from(files) }); }); +app.get(`${RouteStore.imageHierarchyExport}/:hierarchy`, async (req, res) => { + const hierarchy = JSON.parse(req.params.hierarchy); + Object.keys(hierarchy).map(key => { + let value: any; + if (value = hierarchy[key]) { + + } + }); +}); + app.get("/downloadId/:docId", async (req, res) => { res.set('Content-disposition', `attachment;`); res.set('Content-Type', "application/zip"); -- cgit v1.2.3-70-g09d2 From 6411b7fd6b782957050535850154b05c629fd95a Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 6 Oct 2019 20:53:37 -0400 Subject: implemented image hierarchy export to client file system --- src/client/util/Import & Export/ImageUtils.ts | 26 +-------- src/client/views/collections/CollectionView.tsx | 2 + src/server/database.ts | 7 +-- src/server/index.ts | 73 +++++++++++++++++++++++-- src/server/test.txt | 1 + 5 files changed, 76 insertions(+), 33 deletions(-) create mode 100644 src/server/test.txt (limited to 'src/server') diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index bf482aea8..c9abf38fa 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -21,31 +21,11 @@ export namespace ImageUtils { return data !== undefined; }; - export type Hierarchy = { [id: string]: string | Hierarchy }; - - export const ExportHierarchyToFileSystem = async (doc: Doc): Promise => { - const hierarchy: Hierarchy = {}; - await HierarchyTraverserRecursive(doc, hierarchy); + export const ExportHierarchyToFileSystem = async (collection: Doc): Promise => { const a = document.createElement("a"); - a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${JSON.stringify(hierarchy)}`); - a.download = `Full Export of ${StrCast(doc.title)}`; + a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`); + a.download = `Dash Export [${StrCast(collection.title)}].zip`; a.click(); }; - const HierarchyTraverserRecursive = async (collection: Doc, hierarchy: Hierarchy) => { - const children = await DocListCastAsync(collection.data); - if (children) { - const local: Hierarchy = {}; - hierarchy[collection[Id]] = local; - for (const child of children) { - let imageData: Opt; - if (imageData = Cast(child.data, ImageField)) { - local[child[Id]] = imageData.url.href; - } else { - await HierarchyTraverserRecursive(child, local); - } - } - } - }; - } \ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 534246326..893763840 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -18,6 +18,7 @@ import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -123,6 +124,7 @@ export class CollectionView extends React.Component { let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); + ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); } } diff --git a/src/server/database.ts b/src/server/database.ts index d2375ebd9..990441d5a 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -131,7 +131,7 @@ export namespace Database { } } - public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) { + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = "newDocuments") { if (this.db) { this.db.collection(collectionName).findOne({ _id: id }, (err, result) => { if (result) { @@ -165,7 +165,7 @@ export namespace Database { } } - public async visit(ids: string[], fn: (result: any) => string[], collectionName = "newDocuments"): Promise { + public async visit(ids: string[], fn: (result: any) => string[] | Promise, collectionName = "newDocuments"): Promise { if (this.db) { const visited = new Set(); while (ids.length) { @@ -179,10 +179,9 @@ export namespace Database { for (const doc of docs) { const id = doc.id; visited.add(id); - ids.push(...fn(doc)); + ids.push(...(await fn(doc))); } } - } else { return new Promise(res => { this.onConnect.push(() => { diff --git a/src/server/index.ts b/src/server/index.ts index f9ca3de56..526cc6cce 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -54,6 +54,7 @@ import { BatchedArray, TimeUnit } from 'array-batcher'; import { ParsedPDF } from "./PdfTypes"; import { reject } from 'bluebird'; import { ExifData } from 'exif'; +import { Result } from '../client/northstar/model/idea/idea'; const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -320,16 +321,76 @@ app.get("/serializeDoc/:docId", async (req, res) => { res.send({ docs, files: Array.from(files) }); }); -app.get(`${RouteStore.imageHierarchyExport}/:hierarchy`, async (req, res) => { - const hierarchy = JSON.parse(req.params.hierarchy); - Object.keys(hierarchy).map(key => { - let value: any; - if (value = hierarchy[key]) { +export type Hierarchy = { [id: string]: string | Hierarchy }; +export type ZipMutator = (file: Archiver.Archiver) => void | Promise; - } +app.get(`${RouteStore.imageHierarchyExport}/:docId`, async (req, res) => { + const id = req.params.docId; + const hierarchy: Hierarchy = {}; + await targetedVisitorRecursive(id, hierarchy); + BuildAndDispatchZip(res, async zip => { + await hierarchyTraverserRecursive(zip, hierarchy); }); }); +const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise => { + const zip = Archiver('zip'); + zip.pipe(res); + await mutator(zip); + return zip.finalize(); +}; + +const targetedVisitorRecursive = async (seedId: string, hierarchy: Hierarchy): Promise => { + const local: Hierarchy = {}; + const { title, data } = await getData(seedId); + const label = `${title} (${seedId})`; + if (Array.isArray(data)) { + hierarchy[label] = local; + await Promise.all(data.map(proxy => targetedVisitorRecursive(proxy.fieldId, local))); + } else { + hierarchy[label + path.extname(data)] = data; + } +}; + +const getData = async (seedId: string): Promise<{ data: string | any[], title: string }> => { + return new Promise<{ data: string | any[], title: string }>((resolve, reject) => { + Database.Instance.getDocument(seedId, async (result: any) => { + const { data, proto, title } = result.fields; + if (data) { + if (data.url) { + resolve({ data: data.url, title }); + } else if (data.fields) { + resolve({ data: data.fields, title }); + } else { + reject(); + } + } + if (proto) { + getData(proto.fieldId).then(resolve, reject); + } + }); + }); +}; + +const hierarchyTraverserRecursive = async (file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise => { + for (const key of Object.keys(hierarchy)) { + const result = hierarchy[key]; + if (typeof result === "string") { + let path: string; + let matches: RegExpExecArray | null; + if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) { + path = `${__dirname}/public/files/${matches[1]}`; + } else { + const information = await DashUploadUtils.UploadImage(result); + path = information.mediaPaths[0]; + } + file.file(path, { name: key, prefix }); + } else { + await hierarchyTraverserRecursive(file, result, `${prefix}/${key}`); + } + } +}; + app.get("/downloadId/:docId", async (req, res) => { res.set('Content-disposition', `attachment;`); res.set('Content-Type', "application/zip"); diff --git a/src/server/test.txt b/src/server/test.txt new file mode 100644 index 000000000..1c6f6d1f1 --- /dev/null +++ b/src/server/test.txt @@ -0,0 +1 @@ +"{"one":{"two":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg","five":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg","four":{"464d2fbb-8ad9-4bd1-833c-c04f6bb5450f":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"},"three":"http://localhost:1050/files/upload_b94b0710bd7b72c0f201693597a2296a.jpg"}}" \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 45b97084b3d49d521ff39963f250e9cd9efe3f8e Mon Sep 17 00:00:00 2001 From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> Date: Wed, 9 Oct 2019 05:00:23 -0400 Subject: client side google api authentication UI --- src/client/apis/AuthenticationManager.tsx | 90 ++++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 43 ++++++++--- src/client/views/MainView.tsx | 2 + src/server/DashUploadUtils.ts | 2 +- src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 79 +++++++++---------- src/server/apis/google/GooglePhotosUploadUtils.ts | 4 + src/server/index.ts | 31 ++++++-- 8 files changed, 194 insertions(+), 60 deletions(-) create mode 100644 src/client/apis/AuthenticationManager.tsx (limited to 'src/server') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx new file mode 100644 index 000000000..75a50d8f9 --- /dev/null +++ b/src/client/apis/AuthenticationManager.tsx @@ -0,0 +1,90 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; + +@observer +export default class AuthenticationManager extends React.Component<{}> { + public static Instance: AuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + + private get isOpen() { + return this.openState; + } + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private get hasBeenClicked() { + return this.clickedState; + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public executeFullRoutine = async (authenticationLink: string) => { + this.authenticationLink = authenticationLink; + this.isOpen = true; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGooglePhotosAccessToken, { authenticationCode }).then(token => { + this.isOpen = false; + this.hasBeenClicked = false; + resolve(token); + disposer(); + }); + } + } + ); + }); + } + + constructor(props: {}) { + super(props); + AuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + this.hasBeenClicked = true; + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }) + + private get renderPrompt() { + return ( +
    + + {this.clickedState ? : (null)} +
    + ) + } + + render() { + return ( + + ); + } + +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 29cc042b6..dd1492f51 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -13,14 +13,20 @@ import { Docs, DocumentOptions } from "../../documents/Documents"; import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; import { Identified } from "../../Network"; +import AuthenticationManager from "../AuthenticationManager"; +import { List } from "../../../new_fields/List"; export namespace GooglePhotos { + const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + const endpoint = async () => { - const accessToken = await Identified.FetchFromServer(RouteStore.googlePhotosAccessToken); - return new Photos(accessToken); + let response = await Identified.FetchFromServer(RouteStore.readGooglePhotosAccessToken); + if (new RegExp(AuthenticationUrl).test(response)) { + response = await AuthenticationManager.Instance.executeFullRoutine(response); + } + return new Photos(response); }; export enum MediaType { @@ -89,9 +95,14 @@ export namespace GooglePhotos { } const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); const { id, productUrl } = await Create.Album(resolved); - const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey); - if (newMediaItemResults) { - const mediaItems = newMediaItemResults.map(item => item.mediaItem); + const response = await Transactions.UploadImages(images, { id }, descriptionKey); + if (response) { + const { results, failed } = response; + let index: Opt; + while ((index = failed.pop()) !== undefined) { + Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]); + } + const mediaItems: MediaItem[] = results.map(item => item.mediaItem); if (mediaItems.length !== images.length) { throw new AssertionError({ actual: mediaItems.length, expected: images.length }); } @@ -99,6 +110,9 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; + if (!mediaItem) { + continue; + } image.googlePhotosId = mediaItem.id; image.googlePhotosAlbumUrl = productUrl; image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; @@ -304,17 +318,22 @@ export namespace GooglePhotos { }; export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { - const newMediaItems = await UploadImages(sources, album, descriptionKey); - if (!newMediaItems) { + const response = await UploadImages(sources, album, descriptionKey); + if (!response) { return undefined; } - const baseUrls: string[] = await Promise.all(newMediaItems.map(item => { + const baseUrls: string[] = await Promise.all(response.results.map(item => { return new Promise(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); })); return baseUrls; }; - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + export interface ImageUploadResults { + results: NewMediaItemResult[]; + failed: number[]; + } + + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { if (album && "title" in album) { album = await Create.Album(album.title); } @@ -331,8 +350,8 @@ export namespace GooglePhotos { media.push({ url, description }); } if (media.length) { - const uploads: NewMediaItemResult[] = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); - return uploads; + const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + return results; } }; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3b0457dff..12578e5b8 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,6 +39,7 @@ import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { OverlayView } from './OverlayView'; +import AuthenticationManager from '../apis/AuthenticationManager'; @observer export class MainView extends React.Component { @@ -677,6 +678,7 @@ export class MainView extends React.Component {
    {this.dictationOverlay} + {this.mainContent} diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 4230e9b17..57b46714a 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -22,7 +22,7 @@ export namespace DashUploadUtils { const gifs = [".gif"]; const pngs = [".png"]; const jpgs = [".jpg", ".jpeg"]; - const imageFormats = [...pngs, ...jpgs, ...gifs]; + export const imageFormats = [...pngs, ...jpgs, ...gifs]; const videoFormats = [".mov", ".mp4"]; const size = "content-length"; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index ee9cd8a0e..23fdbc53d 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,7 +32,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotosAccessToken = "/googlePhotosAccessToken", + readGooglePhotosAccessToken = "/readGooglePhotosAccessToken", + writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c899c2ef2..2f29cb95f 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -75,6 +75,42 @@ export namespace GoogleApiServerUtils { }); }; + const RetrieveOAuthClient = async (information: CredentialInformation) => { + return new Promise((resolve, reject) => { + readFile(information.credentialsPath, async (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed; + resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0])); + }); + }); + } + + export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { + const client = await RetrieveOAuthClient(information); + return client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES.map(relative => prefix + relative), + }); + }; + + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + const oAuth2Client = await RetrieveOAuthClient(information); + return new Promise((resolve, reject) => { + oAuth2Client.getToken(authenticationCode, async (err, token) => { + if (err || !token) { + reject(err); + return console.error('Error retrieving access token', err); + } + oAuth2Client.setCredentials(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token); + resolve({ token, client: oAuth2Client }); + }); + }); + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { @@ -107,17 +143,13 @@ export namespace GoogleApiServerUtils { return new Promise((resolve, reject) => { // Attempting to authorize user (${userId}) Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { - if (!token) { - // No token registered, so awaiting input from user - return getNewToken(oAuth2Client, userId).then(resolve, reject); - } - if (token.expiry_date! < new Date().getTime()) { + if (token!.expiry_date! < new Date().getTime()) { // Token has expired, so submitting a request for a refreshed access token - return refreshToken(token, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); + return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); } // Authentication successful! - oAuth2Client.setCredentials(token); - resolve({ token, client: oAuth2Client }); + oAuth2Client.setCredentials(token!); + resolve({ token: token!, client: oAuth2Client }); }); }); } @@ -145,35 +177,4 @@ export namespace GoogleApiServerUtils { }); }; - /** - * 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, userId: 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, async (err, token) => { - if (err || !token) { - reject(err); - return console.error('Error retrieving access token', err); - } - oAuth2Client.setCredentials(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token); - resolve({ token, client: oAuth2Client }); - }); - }); - }); - } } \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 16c4f6c3a..4a67e57cc 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { MediaItemCreationResult } from './SharedTypes'; import { NewMediaItem } from "../../index"; import { BatchedArray, TimeUnit } from 'array-batcher'; +import { DashUploadUtils } from '../../DashUploadUtils'; export namespace GooglePhotosUploadUtils { @@ -32,6 +33,9 @@ export namespace GooglePhotosUploadUtils { }; export const DispatchGooglePhotosUpload = async (url: string) => { + if (!DashUploadUtils.imageFormats.includes(path.extname(url))) { + return undefined; + } const body = await request(url, { encoding: null }); const parameters = { method: 'POST', diff --git a/src/server/index.ts b/src/server/index.ts index 690836fff..077002894 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -862,7 +862,22 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.header("userId")! }).then(token => res.send(token))); +app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + const information = { credentialsPath, userId }; + if (!token) { + return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + } + GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); +}); + +app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const information = { credentialsPath, userId }; + const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); + res.send(token.access_token); +}); const tokenError = "Unable to successfully upload bytes for all images!"; const mediaError = "Unable to convert all uploaded bytes to media items!"; @@ -885,16 +900,17 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); - let failed = 0; + let failed: number[] = []; const newMediaItems = await BatchedArray.from(media, { batchSize: 25 }).batchedMapPatientInterval( { magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: GooglePhotosUploadUtils.MediaInput[]) => { const newMediaItems: NewMediaItem[] = []; - for (let element of batch) { + for (let index = 0; index < batch.length; index++) { + const element = batch[index]; const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); if (!uploadToken) { - failed++; + failed.push(index); } else { newMediaItems.push({ description: element.description, @@ -906,12 +922,13 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { } ); - if (failed) { - return _error(res, tokenError); + const failedCount = failed.length; + if (failedCount) { + console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`) } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - result => _success(res, result.newMediaItemResults), + result => _success(res, { results: result.newMediaItemResults, failed }), error => _error(res, mediaError, error) ); }); -- cgit v1.2.3-70-g09d2 From 7763ddefcd14986573f9a0010c7691fa4715b94e Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 9 Oct 2019 15:13:28 -0400 Subject: semicolons --- src/client/util/TooltipTextMenu.tsx | 4 ++-- src/server/apis/google/GoogleApiServerUtils.ts | 4 ++-- src/server/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/server') diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index e162ad475..c82d3bc63 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -841,11 +841,11 @@ export class TooltipTextMenu { } } - update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps) } + update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); } //updates the tooltip menu when the selection changes public updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { if (!view) { - console.log("no editor? why?") + console.log("no editor? why?"); return; } this.view = view; diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 2f29cb95f..963c7736a 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -86,7 +86,7 @@ export namespace GoogleApiServerUtils { resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0])); }); }); - } + }; export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { const client = await RetrieveOAuthClient(information); @@ -109,7 +109,7 @@ export namespace GoogleApiServerUtils { resolve({ token, client: oAuth2Client }); }); }); - } + }; export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { diff --git a/src/server/index.ts b/src/server/index.ts index 077002894..5da05d9a7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -924,7 +924,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { const failedCount = failed.length; if (failedCount) { - console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`) + console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( -- cgit v1.2.3-70-g09d2 From 70a142b84d01e89b56d027b24e37122fa90fe25b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 16:53:16 -0400 Subject: better authentication feedback, cleanup --- src/client/apis/AuthenticationManager.tsx | 90 ---------------- src/client/apis/GoogleAuthenticationManager.scss | 3 + src/client/apis/GoogleAuthenticationManager.tsx | 114 +++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 7 +- src/client/util/SharingManager.tsx | 2 +- src/client/views/MainView.tsx | 4 +- src/server/RouteStore.ts | 4 +- src/server/index.ts | 4 +- 8 files changed, 126 insertions(+), 102 deletions(-) delete mode 100644 src/client/apis/AuthenticationManager.tsx create mode 100644 src/client/apis/GoogleAuthenticationManager.scss create mode 100644 src/client/apis/GoogleAuthenticationManager.tsx (limited to 'src/server') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx deleted file mode 100644 index d8f6b675b..000000000 --- a/src/client/apis/AuthenticationManager.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { observable, action, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import MainViewModal from "../views/MainViewModal"; -import { Opt } from "../../new_fields/Doc"; -import { Identified } from "../Network"; - -const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; -const prompt = "Please paste the external authetication code here..."; - -@observer -export default class AuthenticationManager extends React.Component<{}> { - public static Instance: AuthenticationManager; - @observable private openState = false; - private authenticationLink: Opt = undefined; - @observable private authenticationCode: Opt = undefined; - @observable private clickedState = false; - - private set isOpen(value: boolean) { - runInAction(() => this.openState = value); - } - - private set hasBeenClicked(value: boolean) { - runInAction(() => this.clickedState = value); - } - - public executeFullRoutine = async (service: string) => { - let response = await Identified.FetchFromServer(`/read${service}AccessToken`); - // if this is an authentication url, activate the UI to register the new access token - if (new RegExp(AuthenticationUrl).test(response)) { - this.isOpen = true; - this.authenticationLink = response; - return new Promise(async resolve => { - const disposer = reaction( - () => this.authenticationCode, - authenticationCode => { - if (authenticationCode) { - Identified.PostToServer(`/write${service}AccessToken`, { authenticationCode }).then(token => { - this.isOpen = false; - this.hasBeenClicked = false; - resolve(token); - disposer(); - }); - } - } - ); - }); - } - // otherwise, we already have a valid, stored access token - return response; - } - - constructor(props: {}) { - super(props); - AuthenticationManager.Instance = this; - } - - private handleClick = () => { - window.open(this.authenticationLink); - this.hasBeenClicked = true; - } - - private handlePaste = action((e: React.ChangeEvent) => { - this.authenticationCode = e.currentTarget.value; - }); - - private get renderPrompt() { - return ( -
    - - {this.clickedState ? : (null)} -
    - ); - } - - render() { - return ( - - ); - } - -} \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss new file mode 100644 index 000000000..5efb3ab3b --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -0,0 +1,3 @@ +.paste-target { + padding: 5px; +} \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx new file mode 100644 index 000000000..1ab6380ef --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -0,0 +1,114 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; +import "./GoogleAuthenticationManager.scss"; + +const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; +const prompt = "Paste authorization code here..."; + +@observer +export default class GoogleAuthenticationManager extends React.Component<{}> { + public static Instance: GoogleAuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + @observable private success: Opt = undefined; + @observable private displayLauncher = true; + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public fetchOrGenerateAccessToken = async () => { + let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken); + // if this is an authentication url, activate the UI to register the new access token + if (new RegExp(AuthenticationUrl).test(response)) { + this.isOpen = true; + this.authenticationLink = response; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( + token => { + runInAction(() => this.success = true); + setTimeout(() => { + this.isOpen = false; + runInAction(() => this.displayLauncher = false); + setTimeout(() => { + runInAction(() => this.success = undefined); + runInAction(() => this.displayLauncher = true); + this.hasBeenClicked = false; + }, 500); + }, 1000); + disposer(); + resolve(token); + }, + () => { + this.hasBeenClicked = false; + runInAction(() => this.success = false); + } + ); + } + } + ); + }); + } + // otherwise, we already have a valid, stored access token + return response; + } + + constructor(props: {}) { + super(props); + GoogleAuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + setTimeout(() => this.hasBeenClicked = true, 500); + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }); + + private get renderPrompt() { + return ( +
    + {this.displayLauncher ? : (null)} + {this.clickedState ? : (null)} +
    + ); + } + + render() { + return ( + + ); + } + +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 8e88040db..e93fa6eb4 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -14,14 +14,11 @@ import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/Share import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; import { Identified } from "../../Network"; -import AuthenticationManager from "../AuthenticationManager"; -import { List } from "../../../new_fields/List"; +import GoogleAuthenticationManager from "../GoogleAuthenticationManager"; export namespace GooglePhotos { - const endpoint = async () => { - return new Photos(await AuthenticationManager.Instance.executeFullRoutine("GooglePhotos")); - }; + const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 91c8c572d..1541cd6b2 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -218,7 +218,7 @@ export default class SharingManager extends React.Component<{}> { if (!metadata) { return SharingPermissions.None; } - return StrCast(metadata.permissions, SharingPermissions.None)!; + return StrCast(metadata.permissions, SharingPermissions.None); } private get sharingInterface() { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 12578e5b8..ba4224875 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,7 +39,7 @@ import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { OverlayView } from './OverlayView'; -import AuthenticationManager from '../apis/AuthenticationManager'; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; @observer export class MainView extends React.Component { @@ -678,7 +678,7 @@ export class MainView extends React.Component {
    {this.dictationOverlay} - + {this.mainContent} diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 23fdbc53d..391d9dc0c 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,8 +32,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - readGooglePhotosAccessToken = "/readGooglePhotosAccessToken", - writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken", + readGoogleAccessToken = "/readGoogleAccessToken", + writeGoogleAccessToken = "/writeGoogleAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/index.ts b/src/server/index.ts index 5da05d9a7..dd44a0ce8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -862,7 +862,7 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { +app.get(RouteStore.readGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); const information = { credentialsPath, userId }; @@ -872,7 +872,7 @@ app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); }); -app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => { +app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const information = { credentialsPath, userId }; const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); -- cgit v1.2.3-70-g09d2 From ae9584b6a0b588e0b854a7c5b56723e1a71d47d3 Mon Sep 17 00:00:00 2001 From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> Date: Wed, 9 Oct 2019 16:58:56 -0400 Subject: extraneous document --- src/server/test.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/server/test.txt (limited to 'src/server') diff --git a/src/server/test.txt b/src/server/test.txt deleted file mode 100644 index 1c6f6d1f1..000000000 --- a/src/server/test.txt +++ /dev/null @@ -1 +0,0 @@ -"{"one":{"two":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg","five":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg","four":{"464d2fbb-8ad9-4bd1-833c-c04f6bb5450f":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"},"three":"http://localhost:1050/files/upload_b94b0710bd7b72c0f201693597a2296a.jpg"}}" \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 2cc7ca6b267d9dd6b1683d59b6966a021339bc18 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 9 Oct 2019 22:48:00 -0400 Subject: fixed compile errors and exceptions --- src/client/views/collections/CollectionMasonryViewFieldRow.tsx | 5 ++--- src/client/views/collections/CollectionStackingViewFieldColumn.tsx | 5 ++--- src/server/authentication/config/passport.ts | 2 +- src/server/index.ts | 6 +++--- 4 files changed, 8 insertions(+), 10 deletions(-) (limited to 'src/server') diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 4505aeb70..f01a54001 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -72,8 +72,7 @@ export class CollectionMasonryViewFieldRow extends React.Component { + rowDrop = action((e: Event, de: DragManager.DropEvent) => { this._createAliasSelected = false; if (de.data instanceof DragManager.DocumentDragData) { let key = StrCast(this.props.parent.props.Document.sectionFilter); @@ -87,7 +86,7 @@ export class CollectionMasonryViewFieldRow extends React.Component { + columnDrop = action((e: Event, de: DragManager.DropEvent) => { this._createAliasSelected = false; if (de.data instanceof DragManager.DocumentDragData) { let key = StrCast(this.props.parent.props.Document.sectionFilter); @@ -73,7 +72,7 @@ export class CollectionStackingViewFieldColumn extends React.Component { const provider = req.path.split("/").slice(-1)[0]; - if (_.find(req.user.tokens, { kind: provider })) { + if (_.find((req.user as any).tokens, { kind: provider })) { next(); } else { res.redirect(`/auth/${provider}`); diff --git a/src/server/index.ts b/src/server/index.ts index eeadef239..de46ebf71 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -116,7 +116,7 @@ function addSecureRoute(method: Method, ) { let abstracted = (req: express.Request, res: express.Response) => { if (req.user) { - handler(req.user, res, req); + handler(req.user as any, res, req); } else { req.session!.target = req.originalUrl; onRejection(res, req); @@ -813,8 +813,8 @@ const EndpointHandlerMap = new Map { let sector = req.params.sector; let action = req.params.action; - GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => { - let handler = EndpointHandlerMap.get(action); + GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector as any], { credentials, token }).then(endpoint => { + let handler = EndpointHandlerMap.get(action as any); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( response => res.send(response.data), -- cgit v1.2.3-70-g09d2 From 9505eda799fdf3b0cd2e6bde2d80c18731caee2c Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Thu, 10 Oct 2019 21:36:26 -0400 Subject: switching sidebar to a stacking view... --- src/client/views/DocComponent.tsx | 3 ++- src/client/views/collections/CollectionDockingView.tsx | 16 +++++++++------- src/client/views/collections/CollectionStackingView.tsx | 2 +- src/client/views/collections/CollectionTreeView.tsx | 6 ++++-- src/server/authentication/models/current_user_utils.ts | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) (limited to 'src/server') diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 93e852bef..2c5992259 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -21,7 +21,8 @@ interface DocAnnotatableProps { fieldKey: string; fieldExt: string; whenActiveChanged: (isActive: boolean) => void; - isSelected: () => boolean, renderDepth: number + isSelected: () => boolean; + renderDepth: number; } export function DocAnnotatableComponent

    (schemaCtor: (doc: Doc) => T) { class Component extends React.Component

    { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index fe805a980..1f78c8c97 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -37,7 +37,8 @@ const _global = (window /* browser */ || global /* node */) as any; @observer export class CollectionDockingView extends React.Component { - @observable public static Instance: CollectionDockingView; + @observable public static Instances: CollectionDockingView[] = []; + @computed public static get Instance() { return CollectionDockingView.Instances[0]; } public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) { return { type: 'react-component', @@ -65,7 +66,7 @@ export class CollectionDockingView extends React.Component CollectionDockingView.Instance = this); + runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this)); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; @@ -317,13 +318,14 @@ export class CollectionDockingView extends React.Component this._goldenLayout = null); + this._goldenLayout && this._goldenLayout.destroy(); + runInAction(() => { + CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1); + this._goldenLayout = null; + }); window.removeEventListener('resize', this.onResize); - if (this.reactionDisposer) { - this.reactionDisposer(); - } + this.reactionDisposer && this.reactionDisposer(); } @action onResize = (event: any) => { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 0dfd1d9cf..57722b9b7 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -197,7 +197,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid); return wid * aspect; } - return d.fitWidth ? Math.min(wid * NumCast(d.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : d[HeightSym](); + return d.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : Math.min(wid * NumCast(d.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : d[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 882a0f144..37eb151b1 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -28,6 +28,7 @@ import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; export interface TreeViewProps { @@ -198,6 +199,7 @@ class TreeView extends React.Component { } else { ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); + ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); } ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); @@ -217,7 +219,7 @@ class TreeView extends React.Component { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.document; - DocUtils.MakeLink({doc:sourceDoc}, {doc:destDoc}); + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }); e.stopPropagation(); } if (de.data instanceof DragManager.DocumentDragData) { @@ -538,7 +540,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { + if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); e.stopPropagation(); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 0fbfbf2f3..f3d5555ed 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -99,7 +99,7 @@ export class CurrentUserUtils { } if (doc.sidebar === undefined) { - const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); + const sidebar = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); sidebar.forceActive = true; sidebar.lockedPosition = true; sidebar.gridGap = 5; -- cgit v1.2.3-70-g09d2 From a73a72586c72cd620c16dd7bc0baad88c8e49a34 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Fri, 11 Oct 2019 01:38:48 -0400 Subject: switching search around to left-hand side. --- src/client/documents/DocumentTypes.ts | 3 +- src/client/documents/Documents.ts | 10 +++++ src/client/views/GlobalKeyHandler.ts | 4 ++ src/client/views/MainView.tsx | 10 +++-- .../views/collections/CollectionTreeView.tsx | 5 ++- src/client/views/nodes/DocumentContentsView.tsx | 3 +- src/client/views/nodes/QueryBox.scss | 0 src/client/views/nodes/QueryBox.tsx | 50 ++++++++++++++++++++++ src/client/views/search/FilterBox.tsx | 4 +- src/client/views/search/IconBar.tsx | 14 ------ src/client/views/search/SearchBox.scss | 17 ++++++-- src/client/views/search/SearchBox.tsx | 2 - src/client/views/search/SearchItem.scss | 39 +++++++---------- src/client/views/search/SearchItem.tsx | 19 +++----- .../authentication/models/current_user_utils.ts | 6 +++ 15 files changed, 120 insertions(+), 66 deletions(-) create mode 100644 src/client/views/nodes/QueryBox.scss create mode 100644 src/client/views/nodes/QueryBox.tsx (limited to 'src/server') diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index e5d5885cd..7abaa4043 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -20,5 +20,6 @@ export enum DocumentType { DRAGBOX = "dragbox", PRES = "presentation", LINKFOLLOW = "linkfollow", - PRESELEMENT = "preselement" + PRESELEMENT = "preselement", + QUERY = "search", } \ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 0114f82d8..7f6ab50d8 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -45,6 +45,7 @@ import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; import { LinkFollowBox } from "../views/linking/LinkFollowBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; +import { QueryBox } from "../views/nodes/QueryBox"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); @@ -62,6 +63,7 @@ export interface DocumentOptions { panY?: number; page?: number; scale?: number; + fitWidth?: boolean; layout?: string | Doc; isTemplate?: boolean; templates?: List; @@ -119,6 +121,10 @@ export namespace Docs { layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType }, options: { height: 300, backgroundColor: "black" } }], + [DocumentType.QUERY, { + layout: { view: QueryBox }, + options: { width: 400, fitWidth: true } + }], [DocumentType.IMG, { layout: { view: ImageBox, ext: anno }, options: {} @@ -374,6 +380,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options); } + export function QueryDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.QUERY), "", options); + } + export function TextDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options); } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index c519991a5..6815ff926 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -7,6 +7,8 @@ import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DictationManager } from "../util/DictationManager"; import SharingManager from "../util/SharingManager"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { Cast, PromiseValue } from "../../new_fields/Types"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise; @@ -162,6 +164,8 @@ export default class KeyManager { break; case "f": MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible; + MainView.Instance.flyoutWidth = MainView.Instance.isSearchVisible ? 400 : 0; + PromiseValue(Cast(CurrentUserUtils.UserDocument.searchBox, Doc)).then(pv => pv && (pv.treeViewOpen = (MainView.Instance.flyoutWidth > 0))); break; case "o": let target = SelectionManager.SelectedDocuments()[0]; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ba4224875..1ec21f638 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -12,7 +12,7 @@ import { Id } from '../../new_fields/FieldSymbols'; import { InkTool } from '../../new_fields/InkField'; import { List } from '../../new_fields/List'; import { listSpec } from '../../new_fields/Schema'; -import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types'; +import { BoolCast, Cast, FieldValue, StrCast, PromiseValue } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; @@ -463,7 +463,7 @@ export class MainView extends React.Component { return (null); } return

    : null, + //this.isSearchVisible ?
    : null, ]; } @@ -648,7 +648,9 @@ export class MainView extends React.Component { @observable isSearchVisible = false; @action.bound toggleSearch = () => { - this.isSearchVisible = !this.isSearchVisible; + this.isSearchVisible = !MainView.Instance.isSearchVisible; + MainView.Instance.flyoutWidth = MainView.Instance.isSearchVisible ? 400 : 0; + PromiseValue(Cast(CurrentUserUtils.UserDocument.searchBox, Doc)).then(pv => pv && (pv.treeViewOpen = (MainView.Instance.flyoutWidth > 0))); } @computed private get dictationOverlay() { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 37eb151b1..6f5587b5a 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -260,7 +260,10 @@ class TreeView extends React.Component { let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; + return this.props.document.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : + Math.min(this.docWidth() * NumCast(this.props.document.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth, + NumCast(this.props.containingCollection.height)))) : + NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; })()); } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e4b2ecffd..d4e7c6d4e 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -24,6 +24,7 @@ import { ImageBox } from "./ImageBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; +import { QueryBox } from "./QueryBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; @@ -99,7 +100,7 @@ export class DocumentContentsView extends React.Component { + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(QueryBox, fieldKey); } + _docListChangedReaction: IReactionDisposer | undefined; + componentDidMount() { + } + + componentWillUnmount() { + this._docListChangedReaction && this._docListChangedReaction(); + } + + render() { + return
    + +
    + } +} \ No newline at end of file diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index da733d64b..b841190d4 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -33,7 +33,7 @@ export enum Keys { export class FilterBox extends React.Component { static Instance: FilterBox; - public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; //if true, any keywords can be used. if false, all keywords are required. //this also serves as an indicator if the word status filter is applied @@ -393,7 +393,7 @@ export class FilterBox extends React.Component {
    - {this.getActiveFilters()} + {/* {this.getActiveFilters()} */}
    {this._filterOpen ? (
    diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index c9924222f..bdeb57d5c 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -59,23 +59,9 @@ export class IconBar extends React.Component { render() { return (
    -
    -
    - -
    -
    Select All
    -
    {FilterBox.Instance._allIcons.map((type: string) => )} -
    -
    - -
    -
    Clear
    -
    ); } diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 0dd4d3dc5..bc11604a5 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -1,6 +1,15 @@ @import "../globalCssVariables"; @import "./NaviconButton.scss"; +.searchBox-container { + display: flex; + flex-direction: column; + width:100%; + height:100%; + position: absolute; + font-size: 10px; + line-height: 1; +} .searchBox-bar { height: 32px; display: flex; @@ -49,17 +58,17 @@ } .searchBox-quickFilter { - width: 500px; - margin-left: 25px; + width: 100%; + height: 40px; margin-top: 10px; } .searchBox-results { - margin-right: 136px; + display:flex; + flex-direction: column; top: 300px; display: flex; flex-direction: column; - margin-right: 72px; // height: 560px; height: 100%; // overflow: hidden; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index be75a29e0..62c8c255e 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -346,8 +346,6 @@ export class SearchBox extends React.Component { className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> - -
    {(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) : (
    diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 62715c5eb..9f12994c3 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -2,19 +2,27 @@ .search-overview { display: flex; - flex-direction: row-reverse; + flex-direction: reverse; justify-content: flex-end; z-index: 0; } +.link-count { + background: black; + border-radius: 20px; + color: white; + width: 15px; + text-align: center; + margin-top: 5px; +} .searchBox-placeholder, .search-overview .search-item { - width: 500px; + width: 100%; background: $light-color-secondary; border-color: $intermediate-color; border-bottom-style: solid; padding: 10px; - min-height: 70px; + min-height: 50px; max-height: 150px; height: auto; z-index: 0; @@ -61,16 +69,6 @@ overflow: hidden; position: relative; - .link-count { - opacity: 1; - position: absolute; - z-index: 1000; - text-align: center; - -webkit-transition: opacity 0.2s ease-in-out; - -moz-transition: opacity 0.2s ease-in-out; - -o-transition: opacity 0.2s ease-in-out; - transition: opacity 0.2s ease-in-out; - } .link-extended { // display: none; @@ -112,7 +110,7 @@ .icon-icons, .icon-live { - height: 50px; + height: auto; margin: auto; overflow: hidden; @@ -174,9 +172,7 @@ .searchBox-instances:active { opacity: 1; background: $lighter-alt-accent; - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); + width:150px } .search-item:hover { @@ -193,13 +189,10 @@ .searchBox-instances { float: left; opacity: 1; - width: 150px; + width: 0px; transition: all 0.2s ease; color: black; - transform-origin: top right; - -webkit-transform: scale(0); - -ms-transform: scale(0); - transform: scale(0); + overflow: hidden; } @@ -208,7 +201,7 @@ } .searchBox-placeholder { - min-height: 70px; + min-height: 50px; margin-left: 150px; text-transform: uppercase; text-align: left; diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index a7822ed46..b8cff16f2 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -213,15 +213,6 @@ export class SearchItem extends React.Component { @computed get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; } - @computed - get linkString(): string { - let num = this.linkCount; - if (num === 1) { - return num.toString() + " link"; - } - return num.toString() + " links"; - } - @action pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); } @@ -290,7 +281,11 @@ export class SearchItem extends React.Component {
    -
    +
    +
    +
    {this.linkCount}
    +
    +
    {StrCast(this.props.doc.title)}
    {this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}
    @@ -301,10 +296,6 @@ export class SearchItem extends React.Component {
    {this.DocumentIcon()}
    {this.props.doc.type ? this.props.doc.type : "Other"}
    -
    -
    {this.linkCount}
    -
    {this.linkString}
    -
    diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index f3d5555ed..32da29932 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -97,6 +97,12 @@ export class CurrentUserUtils { curPresentation.boxShadow = "0 0"; doc.curPresentation = curPresentation; } + if (doc.searchBox === undefined) { + const searchBox = Docs.Create.QueryDocument({ title: "Searching" }); + searchBox.boxShadow = "0 0"; + searchBox.ignoreClick = true; + doc.searchBox = searchBox; + } if (doc.sidebar === undefined) { const sidebar = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); -- cgit v1.2.3-70-g09d2 From 11f1fc68532bb75a231b7b697ccd6e5007a2b3f1 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Fri, 11 Oct 2019 08:36:44 -0400 Subject: fixed notifications bell when docs are shared. --- src/server/authentication/models/current_user_utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/server') diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index f3d5555ed..e486ee727 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -46,7 +46,7 @@ export class CurrentUserUtils { // setup workspaces library item if (doc.workspaces === undefined) { - const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces".toUpperCase(), height: 100 }); + const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 }); workspaces.boxShadow = "0 0"; doc.workspaces = workspaces; } @@ -106,6 +106,7 @@ export class CurrentUserUtils { sidebar.xMargin = 5; sidebar.yMargin = 5; sidebar.boxShadow = "1 1 3"; + sidebar.workspaceLibrary = true; // flag that this is the document that shows the Notifications button when documents are shared doc.sidebar = sidebar; } PromiseValue(Cast(doc.sidebar, Doc)).then(sidebar => { -- cgit v1.2.3-70-g09d2 From d95d46e90851141bed17dd30c103be5d0e1e60ab Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 11 Oct 2019 13:43:48 -0400 Subject: search fixes for pdfs --- src/client/views/GlobalKeyHandler.ts | 2 ++ src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/nodes/PDFBox.tsx | 36 ++++++++++++++++++++-- src/client/views/nodes/QueryBox.tsx | 2 +- src/client/views/pdf/PDFViewer.tsx | 7 +++-- src/server/index.ts | 1 - 6 files changed, 43 insertions(+), 7 deletions(-) (limited to 'src/server') diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 6815ff926..557b3e366 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -163,6 +163,8 @@ export default class KeyManager { } break; case "f": + stopPropagation = false; + preventDefault = false; MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible; MainView.Instance.flyoutWidth = MainView.Instance.isSearchVisible ? 400 : 0; PromiseValue(Cast(CurrentUserUtils.UserDocument.searchBox, Doc)).then(pv => pv && (pv.treeViewOpen = (MainView.Instance.flyoutWidth > 0))); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 06d048383..fdbe5339d 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -275,7 +275,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; let pathname = Utils.prepend(file.path); Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - doc && (doc.fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); + doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); doc && this.props.addDocument(doc); }); })); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 9af6d7cad..02a82e0ed 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, runInAction } from 'mobx'; +import { action, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; @@ -36,20 +36,34 @@ export class PDFBox extends DocAnnotatableComponent private _searchString: string = ""; private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title private _pdfViewer: PDFViewer | undefined; + private _searchRef: React.RefObject = React.createRef(); private _keyRef: React.RefObject = React.createRef(); private _valueRef: React.RefObject = React.createRef(); private _scriptRef: React.RefObject = React.createRef(); + private _selectReaction: IReactionDisposer | undefined; @observable private _searching: boolean = false; @observable private _flyout: boolean = false; @observable private _pdf: Opt; @observable private _pageControls = false; + componentWillUnmount() { + this._selectReaction && this._selectReaction(); + } componentDidMount() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); if (pdfUrl instanceof PdfField) { Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); } + this._selectReaction = reaction(() => this.props.isSelected(), + () => { + if (this.props.isSelected()) { + document.removeEventListener("keydown", this.onKeyDown); + document.addEventListener("keydown", this.onKeyDown); + } else { + document.removeEventListener("keydown", this.onKeyDown); + } + }, { fireImmediately: true }); } loaded = (nw: number, nh: number, np: number) => { this.dataDoc.numPages = np; @@ -65,6 +79,22 @@ export class PDFBox extends DocAnnotatableComponent public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); }; public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); } + @undoBatch + onKeyDown = action((e: KeyboardEvent) => { + if (e.key === "f" && e.ctrlKey) { + this._searching = true; + setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100); + e.stopImmediatePropagation(); + e.preventDefault(); + } + if (e.key === "PageDown" || e.key === "ArrowDown" || e.key === "ArrowRight") { + this.forwardPage(); + } + if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") { + this.backPage(); + } + }) + @undoBatch @action private applyFilter = () => { @@ -109,7 +139,9 @@ export class PDFBox extends DocAnnotatableComponent onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}>
    e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
    ; } } \ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index d0759d207..1bae6128c 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -126,7 +126,9 @@ export class PDFViewer extends DocAnnotatableComponent this.props.isSelected(), () => (this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), { fireImmediately: true }); + this._selectionReactionDisposer = reaction(() => this.props.isSelected(), + () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), + { fireImmediately: true }); this._reactionDisposer = reaction( () => this.props.Document.scrollY, (scrollY) => { @@ -655,7 +657,8 @@ export class PDFViewer extends DocAnnotatableComponent this._marqueeing; visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; render() { - return (
    + return (
    {this.pdfViewerDiv} {this.annotationLayer} {this.standinViews} diff --git a/src/server/index.ts b/src/server/index.ts index 62938b9c7..86c226a21 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -213,7 +213,6 @@ const solrURL = "http://localhost:8983/solr/#/dash"; app.get("/textsearch", async (req, res) => { let q = req.query.q; - console.log("TEXTSEARCH " + q); if (q === undefined) { res.send([]); return; -- cgit v1.2.3-70-g09d2 From 3afceef2f8c2be1bddf67cecd97086a41ec6dc48 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 11 Oct 2019 16:47:42 -0400 Subject: changes to menu layouts --- src/client/documents/Documents.ts | 2 + src/client/views/GlobalKeyHandler.ts | 30 +++++- src/client/views/Main.tsx | 5 - src/client/views/MainView.tsx | 108 +++++++++++++-------- .../collections/CollectionMasonryViewFieldRow.tsx | 2 +- .../CollectionStackingViewFieldColumn.tsx | 4 +- src/client/views/nodes/ButtonBox.scss | 2 + src/client/views/nodes/DragBox.tsx | 4 +- src/client/views/nodes/IconBox.tsx | 4 +- src/client/views/search/IconBar.tsx | 2 +- src/client/views/search/IconButton.scss | 2 +- .../authentication/models/current_user_utils.ts | 73 +++++++++++--- 12 files changed, 167 insertions(+), 71 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7f6ab50d8..6b56fb443 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -74,6 +74,7 @@ export interface DocumentOptions { dropAction?: dropActionType; backgroundLayout?: string; chromeStatus?: string; + columnWidth?: number; fontSize?: number; curPage?: number; currentTimecode?: number; @@ -83,6 +84,7 @@ export interface DocumentOptions { dockingConfig?: string; autoHeight?: boolean; dbDoc?: Doc; + icon?: string; // [key: string]: Opt; } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 557b3e366..9d239d0bf 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -9,6 +9,7 @@ import { DictationManager } from "../util/DictationManager"; import SharingManager from "../util/SharingManager"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Cast, PromiseValue } from "../../new_fields/Types"; +import { ScriptField } from "../../new_fields/ScriptField"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise; @@ -162,12 +163,31 @@ export default class KeyManager { } } break; + case "c": + if (MainView.Instance.flyoutWidth > 0) { + MainView.Instance.flyoutWidth = 0; + PromiseValue(Cast(CurrentUserUtils.UserDocument.Library, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + } else { + MainView.Instance.flyoutWidth = 400; + PromiseValue(Cast(CurrentUserUtils.UserDocument.Create, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + } + break; + case "l": + if (MainView.Instance.flyoutWidth > 0) { + MainView.Instance.flyoutWidth = 0; + } else { + MainView.Instance.flyoutWidth = 400; + PromiseValue(Cast(CurrentUserUtils.UserDocument.Library, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + } + break; case "f": - stopPropagation = false; - preventDefault = false; - MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible; - MainView.Instance.flyoutWidth = MainView.Instance.isSearchVisible ? 400 : 0; - PromiseValue(Cast(CurrentUserUtils.UserDocument.searchBox, Doc)).then(pv => pv && (pv.treeViewOpen = (MainView.Instance.flyoutWidth > 0))); + if (MainView.Instance.flyoutWidth > 0) { + MainView.Instance.flyoutWidth = 0; + PromiseValue(Cast(CurrentUserUtils.UserDocument.Library, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + } else { + MainView.Instance.flyoutWidth = 400; + PromiseValue(Cast(CurrentUserUtils.UserDocument.Search, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + } break; case "o": let target = SelectionManager.SelectedDocuments()[0]; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 3bd898ac0..a91a2b69e 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -38,11 +38,6 @@ let swapDocs = async () => { if (info.id !== "__guest__") { // a guest will not have an id registered await CurrentUserUtils.loadUserDocument(info); - // updates old user documents to prevent chrome on tree view. - (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled"; - (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled"; - (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled"; - CurrentUserUtils.UserDocument.chromeStatus = "disabled"; await swapDocs(); } document.getElementById('root')!.addEventListener('wheel', event => { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 1ec21f638..6f60de1c4 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, PromiseValue } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, returnFalse, Utils } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; @@ -40,6 +40,8 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { OverlayView } from './OverlayView'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { CollectionStackingView } from './collections/CollectionStackingView'; +import { ScriptField } from '../../new_fields/ScriptField'; @observer export class MainView extends React.Component { @@ -459,33 +461,63 @@ export class MainView extends React.Component { @computed get flyout() { let sidebar: FieldResult; - if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) { + if (!this.userDoc || !((sidebar = this.userDoc.sidebarContainer) instanceof Doc)) { return (null); } - return - ; + (Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc).columnWidth = this.flyoutWidthFunc() / 3 - 30; + return
    +
    + + +
    +
    + + +
    ; } @computed @@ -493,7 +525,7 @@ export class MainView extends React.Component { if (!this.userDoc) { return (
    {this.dockingContent}
    ); } - let sidebar = this.userDoc.sidebar; + let sidebar = this.userDoc.sidebarContainer; if (!(sidebar instanceof Doc)) { return (null); } @@ -585,16 +617,16 @@ export class MainView extends React.Component { // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); let btns: [React.RefObject, IconName, string, () => Doc | Promise][] = [ - [React.createRef(), "object-group", "Add Collection", addColNode], - [React.createRef(), "tv", "Add Presentation Trail", addPresNode], - [React.createRef(), "globe-asia", "Add Website", addWebNode], - [React.createRef(), "bolt", "Add Button", addButtonDocument], - [React.createRef(), "file", "Add Document Dragger", addDragboxNode], + //[React.createRef(), "object-group", "Add Collection", addColNode], + //[React.createRef(), "tv", "Add Presentation Trail", addPresNode], + //[React.createRef(), "globe-asia", "Add Website", addWebNode], + //[React.createRef(), "bolt", "Add Button", addButtonDocument], + //[React.createRef(), "file", "Add Document Dragger", addDragboxNode], // [React.createRef(), "object-group", "Test Google Photos Search", googlePhotosSearch], - [React.createRef(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode + //[React.createRef(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode //[React.createRef(), "play", "Add Youtube Searcher", addYoutubeSearcher], ]; - if (!ClientUtils.RELEASE) btns.unshift([React.createRef(), "cat", "Add Cat Image", addImageNode]); + //if (!ClientUtils.RELEASE) btns.unshift([React.createRef(), "cat", "Add Cat Image", addImageNode]); return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} > @@ -603,7 +635,7 @@ export class MainView extends React.Component {
      -
    • + {/*
    • */}
    • {btns.map(btn => @@ -612,7 +644,7 @@ export class MainView extends React.Component {
    )} -
  • + {/*
  • */}
  • */} -
  • -
  • - {btns.map(btn => -
  • - -
  • )} - {/*
  • */} -
  • -
  • -
  • -
  • -
  • -
  • -
  • - + {/*
  • */} + + + + {DocListCast(CurrentUserUtils.UserDocument.docButtons).map(doc =>
    + 35 / NumCast(doc.nativeWidth, 35)} + PanelWidth={() => 35} + PanelHeight={() => 35} + renderDepth={0} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne}> + +
    )} + {/*
  • */} + + + + + + +
    ; } @@ -668,16 +674,6 @@ export class MainView extends React.Component { this._colorPickerDisplay = close ? false : !this._colorPickerDisplay; } - /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */ - @computed - get miscButtons() { - return [ - //this.isSearchVisible ?
    : null, - ]; - - } - - @observable isSearchVisible = false; @action.bound toggleSearch = () => { PromiseValue(Cast(CurrentUserUtils.UserDocument.Search, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); @@ -716,7 +712,6 @@ export class MainView extends React.Component { {this.nodesMenu()} - {this.miscButtons}
    diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 6f5587b5a..a325e86c6 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -188,7 +188,7 @@ class TreeView extends React.Component { onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { + if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking && this.props.document !== CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" }); ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index c0d9e1267..af9232c2f 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -2,4 +2,6 @@ transform-origin: left top; position: absolute; background-color: transparent; + top:0; + left:0; } \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 758222e52..20920a9b8 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -614,6 +614,7 @@ export class DocumentView extends DocComponent(Docu DataDoc={this.props.DataDoc} />); } render() { + if (!this.props.Document) return (null); let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined; const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx index 8429382e3..ab3368b94 100644 --- a/src/client/views/nodes/DragBox.tsx +++ b/src/client/views/nodes/DragBox.tsx @@ -51,9 +51,15 @@ export class DragBox extends DocComponent(DragDocu const onDragStart = this.Document.onDragStart; e.stopPropagation(); e.preventDefault(); - let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; - let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); - DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); + DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([this.props.Document]), e.clientX, e.clientY, { + finishDrag: (dropData) => { + let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; + let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); + dropData.droppedDocuments = [doc]; + }, + handlers: { dragComplete: emptyFunction }, + hideSource: false + }); } e.stopPropagation(); e.preventDefault(); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 02a82e0ed..63b412a23 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -93,7 +93,7 @@ export class PDFBox extends DocAnnotatableComponent if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") { this.backPage(); } - }) + }); @undoBatch @action diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index e1bb91838..7408fd92b 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -127,46 +127,38 @@ export class CurrentUserUtils { Search.targetContainer = doc.sidebarContainer; Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox"); - let createCollection = Docs.Create.DragboxDocument({ width: 35, height: 35, title: "Collection", icon: "folder" }); - let createWebPage = Docs.Create.DragboxDocument({ width: 35, height: 35, title: "Web Page", icon: "globe-asia" }); + let createCollection = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" }); + let createWebPage = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })'); - let createCatImage = Docs.Create.DragboxDocument({ width: 35, height: 35, title: "Image", icon: "cat" }); + let createCatImage = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })'); - let createButton = Docs.Create.DragboxDocument({ width: 35, height: 35, title: "Button", icon: "bolt" }); + let createButton = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })'); - let createPresentation = Docs.Create.DragboxDocument({ width: 35, height: 35, title: "Presentation", icon: "tv" }); + let createPresentation = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })'); - let createFolderImport = Docs.Create.DragboxDocument({ width: 35, height: 35, title: "Import Folder", icon: "cloud-upload-alt" }); + let createFolderImport = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); const creators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, height: 50, columnWidth: 35, chromeStatus: "disabled", title: "buttons" }); Create.targetContainer = doc.sidebarContainer; Create.creators = creators; Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); - const buttons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "buttons" }); - buttons.sectionFilter = "title"; - buttons.boxShadow = "0 0"; - buttons.ignoreClick = true; - buttons.hideHeadings = true; - doc.libraryButtons = buttons; + const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "buttons" }); + libraryButtons.sectionFilter = "title"; + libraryButtons.boxShadow = "0 0"; + libraryButtons.ignoreClick = true; + libraryButtons.hideHeadings = true; + libraryButtons.backgroundColor = "lightgrey"; + doc.libraryButtons = libraryButtons; doc.Library = Library; doc.Create = Create; doc.Search = Search; - (Library.onClick as ScriptField).script.run({ this: Library }); - //(doc.sidebarContainer as Doc).proto = library; } - PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { - if (libraryButtons) { - libraryButtons.backgroundColor = "lightgrey"; - } - }); - - PromiseValue(Cast(doc.sidebar, Doc)).then(sidebar => { - if (sidebar) { - sidebar.backgroundColor = "lightgrey"; - } - }); + PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { }); + PromiseValue(Cast(doc.Library, Doc)).then(library => library && library.library && library.targetContainer && (library.onClick as ScriptField).script.run({ this: library })); + PromiseValue(Cast(doc.Create, Doc)).then(async create => create && create.creators && create.targetContainer); + PromiseValue(Cast(doc.Search, Doc)).then(async search => search && search.searchBox && search.targetContainer); if (doc.overlays === undefined) { const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" }); @@ -178,8 +170,7 @@ export class CurrentUserUtils { PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" }))); } - StrCast(doc.title).indexOf("@") !== -1 && (doc.title = (StrCast(doc.title).split("@")[0] + "'s Library").toUpperCase()); - StrCast(doc.title).indexOf("'s Library") !== -1 && (doc.title = StrCast(doc.title).toUpperCase()); + doc.title = "DOCUMENTS"; doc.backgroundColor = "#eeeeee"; doc.width = 100; doc.preventTreeViewOpen = true; -- cgit v1.2.3-70-g09d2 From 34a8b9dd402a247e7ad0a57115935b1e3a04a8d3 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 12 Oct 2019 12:25:02 -0400 Subject: cleaned up notifs and bottom menu icons --- src/client/views/Main.scss | 21 +++----- src/client/views/MainView.tsx | 57 +++++++++------------- src/client/views/MainViewNotifs.scss | 18 +++++++ src/client/views/MainViewNotifs.tsx | 32 ++++++++++++ .../views/collections/CollectionTreeView.tsx | 26 +--------- .../authentication/models/current_user_utils.ts | 1 - 6 files changed, 79 insertions(+), 76 deletions(-) create mode 100644 src/client/views/MainViewNotifs.scss create mode 100644 src/client/views/MainViewNotifs.tsx (limited to 'src/server') diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 705da7b35..debb941c8 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -93,21 +93,6 @@ button:hover { right: 0px; } -.main-notifs-badge { - position: absolute; - top: -10px; - right: -10px; - color: white; - background: #f44b42; - font-weight: 300; - border-radius: 100%; - width: 25px; - height: 25px; - text-align: center; - padding-top: 4px; - font-size: 12px; -} - //toolbar stuff #toolbar { position: absolute; @@ -237,6 +222,12 @@ ul#add-options-list { padding: 0; } } +.mainView-logout { + position: absolute; + right: 0; + bottom: 0; + font-size: 8px; +} .mainView-libraryFlyout { height: 100%; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 1fa231659..e8ffa5987 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -16,7 +16,7 @@ import { ScriptField } from '../../new_fields/ScriptField'; import { BoolCast, Cast, FieldValue, PromiseValue, StrCast, NumCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; @@ -40,6 +40,7 @@ import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; +import { MainViewNotifs } from './MainViewNotifs'; @observer export class MainView extends React.Component { @@ -47,6 +48,7 @@ export class MainView extends React.Component { @observable addMenuToggle = React.createRef(); @observable public pwidth: number = 0; @observable public pheight: number = 0; + private _buttonBarHeight = 85; private dropDisposer?: DragManager.DragDropDisposer; @observable private dictationState = DictationManager.placeholder; @@ -138,24 +140,6 @@ export class MainView extends React.Component { firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - - if (this.userDoc) { - reaction(() => { - let workspaces = this.userDoc.workspaces; - let recent = this.userDoc.recentlyClosed; - if (!(recent instanceof Doc)) return 0; - if (!(workspaces instanceof Doc)) return 0; - let workspacesDoc = workspaces; - let recentDoc = recent; - let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + this.userDoc[HeightSym]() * 0.00001; - return libraryHeight; - }, (libraryHeight: number) => { - if (libraryHeight && Math.abs(this.userDoc[HeightSym]() - libraryHeight) > 5) { - this.userDoc.height = libraryHeight; - } - (Cast(this.userDoc.recentlyClosed, Doc) as Doc).allowClear = true; - }, { fireImmediately: true }); - } } componentWillUnMount() { @@ -340,7 +324,7 @@ export class MainView extends React.Component { if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) { const l = Cast(col.data, listSpec(Doc)); if (l) { - runInAction(() => CollectionTreeView.NotifsCol = col); + runInAction(() => MainViewNotifs.NotifsCol = col); } } }, 100); @@ -349,7 +333,7 @@ export class MainView extends React.Component { drop = action((e: Event, de: DragManager.DropEvent) => { (de.data as DragManager.DocumentDragData).draggedDocuments.map(doc => Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc)); - }) + }); onDrop = (e: React.DragEvent) => { e.preventDefault(); @@ -368,6 +352,9 @@ export class MainView extends React.Component { getPHeight = () => { return this.pheight; } + getContentsHeight = () => { + return this.pheight - this._buttonBarHeight; + } @observable flyoutWidth: number = 250; @observable flyoutTranslate: boolean = true; @@ -465,12 +452,12 @@ export class MainView extends React.Component { if (!(sidebarContent instanceof Doc)) { return (null); } - (Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc).columnWidth = this.flyoutWidthFunc() / 3 - 30; - let buttonBarHeight = 85; + let libraryButtonDoc = Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc; + libraryButtonDoc.columnWidth = this.flyoutWidthFunc() / 3 - 30; return
    -
    +
    -
    +
    +
    ; } @computed get mainContent() { - if (!this.userDoc) { - return (
    {this.dockingContent}
    ); - } - let sidebar = this.userDoc.sidebarContainer; - if (!(sidebar instanceof Doc)) { + let sidebar = this.userDoc && this.userDoc.sidebarContainer; + if (!this.userDoc || !(sidebar instanceof Doc)) { return (null); } return ( @@ -617,6 +604,7 @@ export class MainView extends React.Component { return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} > + @@ -632,7 +620,7 @@ export class MainView extends React.Component { addDocument={undefined} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} - removeDocument={undefined} + removeDocument={(doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument, "docButtons", doc)} ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} @@ -662,7 +650,6 @@ export class MainView extends React.Component { -
    ; } diff --git a/src/client/views/MainViewNotifs.scss b/src/client/views/MainViewNotifs.scss new file mode 100644 index 000000000..25ec95643 --- /dev/null +++ b/src/client/views/MainViewNotifs.scss @@ -0,0 +1,18 @@ +.mainNotifs-container { + position:absolute; + + .mainNotifs-badge { + position: absolute; + top: -10px; + right: -10px; + color: white; + background: #f44b42; + font-weight: 300; + border-radius: 100%; + width: 25px; + height: 25px; + text-align: center; + padding-top: 4px; + font-size: 12px; + } +} \ No newline at end of file diff --git a/src/client/views/MainViewNotifs.tsx b/src/client/views/MainViewNotifs.tsx new file mode 100644 index 000000000..09fa1cb0c --- /dev/null +++ b/src/client/views/MainViewNotifs.tsx @@ -0,0 +1,32 @@ +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import "normalize.css"; +import * as React from 'react'; +import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; +import { emptyFunction } from '../../Utils'; +import { SetupDrag } from '../util/DragManager'; +import "./MainViewNotifs.scss"; +import { CollectionDockingView } from './collections/CollectionDockingView'; + + +@observer +export class MainViewNotifs extends React.Component { + + @observable static NotifsCol: Opt; + openNotifsCol = () => { + if (MainViewNotifs.NotifsCol) { + CollectionDockingView.AddRightSplit(MainViewNotifs.NotifsCol, undefined); + } + } + render() { + const length = MainViewNotifs.NotifsCol ? DocListCast(MainViewNotifs.NotifsCol.data).length : 0; + const notifsRef = React.createRef(); + const dragNotifs = action(() => MainViewNotifs.NotifsCol!); + return
    + +
    ; + } +} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index a325e86c6..abaa9662c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -517,8 +517,6 @@ export class CollectionTreeView extends CollectionSubView(Document) { private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; - @observable static NotifsCol: Opt; - @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } protected createTreeDropTarget = (ele: HTMLDivElement) => { @@ -557,31 +555,10 @@ export class CollectionTreeView extends CollectionSubView(Document) { } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); - openNotifsCol = () => { - if (CollectionTreeView.NotifsCol) { - this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight"); - } - } - @computed get renderNotifsButton() { - const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0; - const notifsRef = React.createRef(); - const dragNotifs = action(() => CollectionTreeView.NotifsCol!); - return
    -
    - -
    0 ? { "display": "initial" } : { "display": "none" }}> - {length} -
    -
    -
    ; - } @computed get renderClearButton() { return
    - @@ -614,7 +591,6 @@ export class CollectionTreeView extends CollectionSubView(Document) { TreeView.loadId = doc[Id]; Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); })} /> - {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)} {this.props.Document.allowClear ? this.renderClearButton : (null)}
      { diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 7408fd92b..874f49f10 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -115,7 +115,6 @@ export class CurrentUserUtils { library.xMargin = 5; library.yMargin = 5; library.boxShadow = "1 1 3"; - library.workspaceLibrary = true; // flag that this is the document that shows the Notifications button when documents are shared Library.targetContainer = doc.sidebarContainer; Library.library = library; Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library"); -- cgit v1.2.3-70-g09d2 From 8910a2649a1b2edea9843df1b780937ad0e69fb5 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 12 Oct 2019 13:09:53 -0400 Subject: color picker as document in sidebar now. --- src/client/documents/DocumentTypes.ts | 1 + src/client/documents/Documents.ts | 10 ++++++++++ src/client/util/DocumentManager.ts | 6 ++---- src/client/views/GlobalKeyHandler.ts | 4 ++-- src/client/views/MainView.tsx | 7 +------ src/client/views/nodes/ColorBox.scss | 10 ++++++++++ src/client/views/nodes/ColorBox.tsx | 16 ++++++++++++++++ src/client/views/nodes/DocumentContentsView.tsx | 7 ++++++- src/client/views/nodes/QueryBox.tsx | 19 ++----------------- .../authentication/models/current_user_utils.ts | 4 +++- 10 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 src/client/views/nodes/ColorBox.scss create mode 100644 src/client/views/nodes/ColorBox.tsx (limited to 'src/server') diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 7abaa4043..dad2de0b5 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -22,4 +22,5 @@ export enum DocumentType { LINKFOLLOW = "linkfollow", PRESELEMENT = "preselement", QUERY = "search", + COLOR = "color", } \ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6b56fb443..e783848ff 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -46,6 +46,7 @@ import { DocumentType } from "./DocumentTypes"; import { LinkFollowBox } from "../views/linking/LinkFollowBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; import { QueryBox } from "../views/nodes/QueryBox"; +import { ColorBox } from "../views/nodes/ColorBox"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); @@ -69,6 +70,7 @@ export interface DocumentOptions { templates?: List; viewType?: number; backgroundColor?: string; + ignoreClick?: boolean; opacity?: number; defaultBackgroundColor?: string; dropAction?: dropActionType; @@ -127,6 +129,10 @@ export namespace Docs { layout: { view: QueryBox }, options: { width: 400, fitWidth: true } }], + [DocumentType.COLOR, { + layout: { view: ColorBox }, + options: { nativeWidth: 220, nativeHeight: 300 } + }], [DocumentType.IMG, { layout: { view: ImageBox, ext: anno }, options: {} @@ -386,6 +392,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.QUERY), "", options); } + export function ColorDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options); + } + export function TextDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 24285a70a..00de39671 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,16 +1,14 @@ -import { action, computed, observable, trace } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { Doc, DocListCastAsync } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; +import { List } from '../../new_fields/List'; import { Cast, NumCast, StrCast } from '../../new_fields/Types'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; -import { undoBatch, UndoManager } from './UndoManager'; import { Scripting } from './Scripting'; -import { List } from '../../new_fields/List'; import { SelectionManager } from './SelectionManager'; -import { notDeepEqual } from 'assert'; export class DocumentManager { diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 82f5a573c..f3e1933d7 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -165,10 +165,10 @@ export default class KeyManager { break; case "c": PromiseValue(Cast(CurrentUserUtils.UserDocument.Create, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 75) { + if (MainView.Instance.flyoutWidth === 240) { MainView.Instance.flyoutWidth = 0; } else { - MainView.Instance.flyoutWidth = 75; + MainView.Instance.flyoutWidth = 240; } break; case "l": diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index e8ffa5987..cc412a609 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -5,7 +5,6 @@ import { action, computed, configure, observable, reaction, runInAction } from ' import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import { SketchPicker } from 'react-color'; import Measure from 'react-measure'; import { Doc, DocListCast, Field, FieldResult, HeightSym, Opt } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; @@ -640,11 +639,7 @@ export class MainView extends React.Component {
    )} {/*
  • */} - + diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss new file mode 100644 index 000000000..8df617fca --- /dev/null +++ b/src/client/views/nodes/ColorBox.scss @@ -0,0 +1,10 @@ +.colorBox-container { + width:100%; + height:100%; + position: relative; + pointer-events:all; + + .sketch-picker { + margin:auto; + } +} \ No newline at end of file diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx new file mode 100644 index 000000000..4aff770f9 --- /dev/null +++ b/src/client/views/nodes/ColorBox.tsx @@ -0,0 +1,16 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { SketchPicker } from 'react-color'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ColorBox.scss"; +import { InkingControl } from "../InkingControl"; + +@observer +export class ColorBox extends React.Component { + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); } + render() { + return
    + +
    ; + } +} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index d4e7c6d4e..f2a581c42 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -25,6 +25,7 @@ import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; import { QueryBox } from "./QueryBox"; +import { ColorBox } from "./ColorBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; @@ -100,7 +101,11 @@ export class DocumentContentsView extends React.Component(), { width: 200, height: 500, title: "a presentation trail" })'); let createFolderImport = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); - const creators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, height: 50, columnWidth: 35, chromeStatus: "disabled", title: "buttons" }); + const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, chromeStatus: "disabled", title: "buttons" }); + const color = Docs.Create.ColorDocument({ title: "color picker", width: 400, ignoreClick: true }); + const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "buttons" }) Create.targetContainer = doc.sidebarContainer; Create.creators = creators; Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); -- cgit v1.2.3-70-g09d2 From ed89b0fbf26c5bf4263198938dae07e6f32ab436 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 12 Oct 2019 13:43:13 -0400 Subject: a few more tweaks and now the bottom buttons are customizable --- src/client/views/MainView.tsx | 11 ++++++++++- src/client/views/nodes/DragBox.tsx | 5 +++-- src/new_fields/Doc.ts | 1 + src/server/authentication/models/current_user_utils.ts | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) (limited to 'src/server') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index cc412a609..18e5052b7 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -40,6 +40,7 @@ import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { MainViewNotifs } from './MainViewNotifs'; +import { DocumentType } from '../documents/DocumentTypes'; @observer export class MainView extends React.Component { @@ -331,7 +332,15 @@ export class MainView extends React.Component { } drop = action((e: Event, de: DragManager.DropEvent) => { - (de.data as DragManager.DocumentDragData).draggedDocuments.map(doc => Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc)); + (de.data as DragManager.DocumentDragData).draggedDocuments.map(doc => { + if (doc.type !== DocumentType.DRAGBOX) { + let dbox = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); + dbox.factory = doc; + dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.factory, true)'); + doc = dbox; + } + Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc); + }); }); onDrop = (e: React.DragEvent) => { diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx index ab3368b94..4fca96382 100644 --- a/src/client/views/nodes/DragBox.tsx +++ b/src/client/views/nodes/DragBox.tsx @@ -44,7 +44,7 @@ export class DragBox extends DocComponent(DragDocu } } - onDragMove = (e: MouseEvent) => { + onDragMove = async (e: MouseEvent) => { if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { document.removeEventListener("pointermove", this.onDragMove); document.removeEventListener("pointerup", this.onDragUp); @@ -52,7 +52,7 @@ export class DragBox extends DocComponent(DragDocu e.stopPropagation(); e.preventDefault(); DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([this.props.Document]), e.clientX, e.clientY, { - finishDrag: (dropData) => { + finishDrag: async (dropData) => { let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); dropData.droppedDocuments = [doc]; @@ -60,6 +60,7 @@ export class DragBox extends DocComponent(DragDocu handlers: { dragComplete: emptyFunction }, hideSource: false }); + await this.props.Document.factory; // if there's a factory Doc that is being copied, make sure it's not pending. } e.stopPropagation(); e.preventDefault(); diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index eb752f8c6..66036f673 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -730,6 +730,7 @@ export namespace Doc { Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; }); Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); +Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return Doc.MakeCopy(doc, copyProto); }); Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); Scripting.addGlobal(function aliasDocs(field: any) { return new List(field.map((d: any) => Doc.MakeAlias(d))); }); Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); \ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 09bab959d..9a7b6442b 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -130,7 +130,7 @@ export class CurrentUserUtils { let createWebPage = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })'); let createCatImage = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); - createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })'); + createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })'); let createButton = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })'); let createPresentation = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); @@ -139,7 +139,7 @@ export class CurrentUserUtils { createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, chromeStatus: "disabled", title: "buttons" }); const color = Docs.Create.ColorDocument({ title: "color picker", width: 400, ignoreClick: true }); - const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "buttons" }) + const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "buttons" }); Create.targetContainer = doc.sidebarContainer; Create.creators = creators; Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); -- cgit v1.2.3-70-g09d2 From d7653cab93309717488d51c68c8b1f0db64b949c Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 12 Oct 2019 13:59:32 -0400 Subject: fixed dropAction for library --- src/server/authentication/models/current_user_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/server') diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 9a7b6442b..08cdee0fd 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -30,7 +30,6 @@ export class CurrentUserUtils { private static createUserDocument(id: string): Doc { let doc = new Doc(id, true); doc.viewType = CollectionViewType.Tree; - doc.dropAction = "alias"; doc.layout = CollectionView.LayoutString(); doc.title = Doc.CurrentUserEmail; this.updateUserDocument(doc); @@ -114,6 +113,7 @@ export class CurrentUserUtils { library.gridGap = 5; library.xMargin = 5; library.yMargin = 5; + library.dropAction = "alias"; library.boxShadow = "1 1 3"; Library.targetContainer = doc.sidebarContainer; Library.library = library; -- cgit v1.2.3-70-g09d2 From f16621ff6bc0ad30cae39872412de80aa9f5e25c Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 12 Oct 2019 15:07:28 -0400 Subject: dragbox improvements to control creating aliases/copies --- src/client/util/DragManager.ts | 3 ++- src/client/views/nodes/DocumentView.tsx | 9 ++++++--- src/client/views/nodes/DragBox.tsx | 17 +++++++++++++---- src/server/authentication/models/current_user_utils.ts | 5 ++++- 4 files changed, 25 insertions(+), 9 deletions(-) (limited to 'src/server') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index ddc8fb62c..7259f66e3 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -10,7 +10,6 @@ import { LinkManager } from "./LinkManager"; import { SelectionManager } from "./SelectionManager"; import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; import { Docs } from "../documents/Documents"; -import { CompileScript } from "./Scripting"; import { ScriptField } from "../../new_fields/ScriptField"; import { List } from "../../new_fields/List"; import { PrefetchProxy } from "../../new_fields/Proxy"; @@ -208,6 +207,7 @@ export namespace DragManager { } draggedDocuments: Doc[]; droppedDocuments: Doc[]; + removeDropProperties: string[] = []; offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; @@ -244,6 +244,7 @@ export namespace DragManager { dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) : dragData.draggedDocuments ); + dragData.removeDropProperties.map(prop => dropData.droppedDocuments.map((d: Doc) => d[prop] = undefined)); }); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 20920a9b8..b98627c66 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -7,7 +7,7 @@ import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc import { Id } from '../../../new_fields/FieldSymbols'; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { emptyFunction, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; @@ -102,6 +102,8 @@ export const documentSchema = createSchema({ height: "number", // " backgroundColor: "string", // background color of document opacity: "number", // opacity of document + dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy") + removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped (used for dragBox things) onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents @@ -167,9 +169,10 @@ export class DocumentView extends DocComponent(Docu dragData.dropAction = dropAction; dragData.moveDocument = this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; + dragData.removeDropProperties = this.Document.removeDropProperties || []; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { - dragComplete: action(emptyFunction) + dragComplete: action((emptyFunction)) }, hideSource: !dropAction }); @@ -260,7 +263,7 @@ export class DocumentView extends DocComponent(Docu if (!e.altKey && !this.topMost && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx index 4fca96382..1fd6cb5a5 100644 --- a/src/client/views/nodes/DragBox.tsx +++ b/src/client/views/nodes/DragBox.tsx @@ -3,7 +3,7 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../../new_fields/Doc'; -import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; import { ScriptField } from '../../../new_fields/ScriptField'; import { emptyFunction } from '../../../Utils'; import { CompileScript } from '../../util/Scripting'; @@ -17,6 +17,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import { DragManager } from '../../util/DragManager'; import { Docs } from '../../documents/Documents'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Cast } from '../../../new_fields/Types'; +import { ContextMenuProps } from '../ContextMenuItem'; library.add(faEdit as any); @@ -51,16 +53,19 @@ export class DragBox extends DocComponent(DragDocu const onDragStart = this.Document.onDragStart; e.stopPropagation(); e.preventDefault(); - DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([this.props.Document]), e.clientX, e.clientY, { + let dragData = new DragManager.DocumentDragData([this.props.Document]); + const factory = await Cast(this.props.Document.factory, Doc);// if there's a factory Doc that is being copied, make sure it's not pending. + dragData.removeDropProperties = factory ? Cast(factory.removeDropProperties, listSpec("string"), []) : []; + DragManager.StartDocumentDrag([this._mainCont.current!], dragData, e.clientX, e.clientY, { finishDrag: async (dropData) => { let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); dropData.droppedDocuments = [doc]; + dragData.removeDropProperties.map(prop => dropData.droppedDocuments.map((d: Doc) => d[prop] = undefined)); }, handlers: { dragComplete: emptyFunction }, hideSource: false }); - await this.props.Document.factory; // if there's a factory Doc that is being copied, make sure it's not pending. } e.stopPropagation(); e.preventDefault(); @@ -72,7 +77,10 @@ export class DragBox extends DocComponent(DragDocu } onContextMenu = () => { - ContextMenu.Instance.addItem({ + let funcs: ContextMenuProps[] = []; + funcs.push({ description: "Set Make Aliases", icon: "edit", event: () => this.props.Document.factory && (this.props.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.factory)')) }); + funcs.push({ description: "Set Make Copies", icon: "edit", event: () => this.props.Document.factory && (this.props.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.factory, true)')) }); + funcs.push({ description: "Edit OnClick script", icon: "edit", event: () => { let overlayDisposer: () => void = emptyFunction; const script = this.Document.onDragStart; @@ -96,6 +104,7 @@ export class DragBox extends DocComponent(DragDocu overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` }); } }); + ContextMenu.Instance.addItem({ description: "DragBox Funcs...", subitems: funcs, icon: "asterisk" }); } render() { diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 08cdee0fd..74a41d8cf 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -138,7 +138,10 @@ export class CurrentUserUtils { let createFolderImport = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, chromeStatus: "disabled", title: "buttons" }); - const color = Docs.Create.ColorDocument({ title: "color picker", width: 400, ignoreClick: true }); + const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 }); + color.dropAction = "alias"; + color.ignoreClick = true; + color.removeDropProperties = new List(["dropAction", "ignoreClick"]); const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "buttons" }); Create.targetContainer = doc.sidebarContainer; Create.creators = creators; -- cgit v1.2.3-70-g09d2 From 5b83d8da6c262897dc75ada26f08ed1c46ceb95c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 12 Oct 2019 19:13:25 -0400 Subject: parse google user data and use it to provide customized feedback on login --- src/client/apis/GoogleAuthenticationManager.scss | 20 +++++++++- src/client/apis/GoogleAuthenticationManager.tsx | 29 +++++++++++--- src/server/apis/google/GoogleApiServerUtils.ts | 51 +++++++++++++++++++++--- src/server/database.ts | 5 ++- src/server/index.ts | 3 +- 5 files changed, 91 insertions(+), 17 deletions(-) (limited to 'src/server') diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss index 5efb3ab3b..13bde822d 100644 --- a/src/client/apis/GoogleAuthenticationManager.scss +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -1,3 +1,19 @@ -.paste-target { - padding: 5px; +.authorize-container { + display: flex; + flex-direction: column; + align-items: center; + + .paste-target { + padding: 5px; + width: 100%; + } + + .avatar { + border-radius: 50%; + } + + .welcome { + font-style: italic; + margin-top: 15px; + } } \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index d143d8273..01dac3996 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -19,6 +19,8 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { @observable private clickedState = false; @observable private success: Opt = undefined; @observable private displayLauncher = true; + @observable private avatar: Opt = undefined; + @observable private username: Opt = undefined; private set isOpen(value: boolean) { runInAction(() => this.openState = value); @@ -40,10 +42,14 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { authenticationCode => { if (authenticationCode) { Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( - token => { + ({ access_token, avatar, name }) => { + runInAction(() => { + this.avatar = avatar; + this.username = name; + }); this.beginFadeout(); disposer(); - resolve(token); + resolve(access_token); }, action(() => { this.hasBeenClicked = false; @@ -61,15 +67,18 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { beginFadeout = action(() => { this.success = true; + this.authenticationCode = undefined; + this.displayLauncher = false; + this.hasBeenClicked = false; setTimeout(action(() => { this.isOpen = false; - this.displayLauncher = false; setTimeout(action(() => { this.success = undefined; this.displayLauncher = true; - this.hasBeenClicked = false; + this.avatar = undefined; + this.username = undefined; }), 500); - }), 2000); + }), 3000); }); constructor(props: {}) { @@ -88,7 +97,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { private get renderPrompt() { return ( -
    +
    {this.displayLauncher ?
    ); } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 963c7736a..5714c9928 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -25,7 +25,8 @@ export namespace GoogleApiServerUtils { 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', - 'photoslibrary.sharing' + 'photoslibrary.sharing', + 'userinfo.profile' ]; export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); @@ -96,21 +97,61 @@ export namespace GoogleApiServerUtils { }); }; - export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + export interface GoogleAuthenticationResult { + access_token: string; + avatar: string; + name: string; + } + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { const oAuth2Client = await RetrieveOAuthClient(information); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { oAuth2Client.getToken(authenticationCode, async (err, token) => { if (err || !token) { reject(err); return console.error('Error retrieving access token', err); } oAuth2Client.setCredentials(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token); - resolve({ token, client: oAuth2Client }); + const enriched = injectUserInfo(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched); + const { given_name, picture } = enriched.userInfo; + resolve({ + access_token: enriched.access_token!, + avatar: picture, + name: given_name + }); }); }); }; + /** + * It's pretty cool: the credentials id_token is split into thirds by periods. + * The middle third contains a base64-encoded JSON string with all the + * user info contained in the interface below. So, we isolate that middle third, + * base64 decode with atob and parse the JSON. + * @param credentials the client credentials returned from OAuth after the user + * has executed the authentication routine + */ + const injectUserInfo = (credentials: Credentials): EnrichedCredentials => { + const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); + return { ...credentials, userInfo }; + }; + + export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; + export interface UserInfo { + at_hash: string; + aud: string; + azp: string; + exp: number; + family_name: string; + given_name: string; + iat: number; + iss: string; + locale: string; + name: string; + picture: string; + sub: string; + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { diff --git a/src/server/database.ts b/src/server/database.ts index 990441d5a..db86b472d 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -4,6 +4,7 @@ import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { DashUploadUtils } from './DashUploadUtils'; import { Credentials } from 'google-auth-library'; +import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; export namespace Database { @@ -259,8 +260,8 @@ export namespace Database { return SanitizedSingletonQuery({ userId }, GoogleAuthentication, removeId); }; - export const Write = async (userId: string, token: any) => { - return Instance.insert({ userId, canAccess: [], ...token }, GoogleAuthentication); + export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => { + return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication); }; export const Update = async (userId: string, access_token: string, expiry_date: number) => { diff --git a/src/server/index.ts b/src/server/index.ts index 86c226a21..010a851bc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -958,8 +958,7 @@ app.get(RouteStore.readGoogleAccessToken, async (req, res) => { app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const information = { credentialsPath, userId }; - const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); - res.send(token.access_token); + res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); }); const tokenError = "Unable to successfully upload bytes for all images!"; -- cgit v1.2.3-70-g09d2 From 988822bdeeed82b0c1ef611f2f7a3111d029d564 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 12 Oct 2019 19:37:06 -0400 Subject: a bunch of fixes to clean up drag box stuff. --- src/client/documents/DocumentTypes.ts | 2 +- src/client/documents/Documents.ts | 14 +- src/client/util/DragManager.ts | 19 +- src/client/views/GlobalKeyHandler.ts | 1 - src/client/views/Main.scss | 17 +- src/client/views/MainView.tsx | 250 ++++++++------------- src/client/views/nodes/ButtonBox.tsx | 3 +- src/client/views/nodes/DocumentContentsView.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 27 ++- src/client/views/nodes/DragBox.scss | 13 -- src/client/views/nodes/DragBox.tsx | 115 ---------- src/client/views/nodes/FontIconBox.scss | 13 ++ src/client/views/nodes/FontIconBox.tsx | 21 ++ .../authentication/models/current_user_utils.ts | 29 ++- 14 files changed, 194 insertions(+), 334 deletions(-) delete mode 100644 src/client/views/nodes/DragBox.scss delete mode 100644 src/client/views/nodes/DragBox.tsx create mode 100644 src/client/views/nodes/FontIconBox.scss create mode 100644 src/client/views/nodes/FontIconBox.tsx (limited to 'src/server') diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index dad2de0b5..432e53825 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -17,7 +17,7 @@ export enum DocumentType { TEMPLATE = "template", EXTENSION = "extension", YOUTUBE = "youtube", - DRAGBOX = "dragbox", + FONTICONBOX = "fonticonbox", PRES = "presentation", LINKFOLLOW = "linkfollow", PRESELEMENT = "preselement", diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e783848ff..1f89d2993 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -37,7 +37,7 @@ import { DocumentManager } from "../util/DocumentManager"; import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox"; import { Scripting, CompileScript } from "../util/Scripting"; import { ButtonBox } from "../views/nodes/ButtonBox"; -import { DragBox } from "../views/nodes/DragBox"; +import { FontIconBox } from "../views/nodes/FontIconBox"; import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField"; import { PresBox } from "../views/nodes/PresBox"; import { ComputedField } from "../../new_fields/ScriptField"; @@ -71,6 +71,7 @@ export interface DocumentOptions { viewType?: number; backgroundColor?: string; ignoreClick?: boolean; + lockedPosition?: boolean; opacity?: number; defaultBackgroundColor?: string; dropAction?: dropActionType; @@ -82,6 +83,7 @@ export interface DocumentOptions { currentTimecode?: number; documentText?: string; borderRounding?: string; + boxShadow?: string; schemaColumns?: List; dockingConfig?: string; autoHeight?: boolean; @@ -127,7 +129,7 @@ export namespace Docs { }], [DocumentType.QUERY, { layout: { view: QueryBox }, - options: { width: 400, fitWidth: true } + options: { width: 400 } }], [DocumentType.COLOR, { layout: { view: ColorBox }, @@ -183,8 +185,8 @@ export namespace Docs { layout: { view: PresBox }, options: {} }], - [DocumentType.DRAGBOX, { - layout: { view: DragBox }, + [DocumentType.FONTICONBOX, { + layout: { view: FontIconBox }, options: { width: 40, height: 40 }, }], [DocumentType.LINKFOLLOW, { @@ -476,8 +478,8 @@ export namespace Docs { } - export function DragboxDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), undefined, { ...(options || {}) }); + export function FontIconDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.FONTICONBOX), undefined, { ...(options || {}) }); } export function LinkFollowBoxDocument(options?: DocumentOptions) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 7259f66e3..2c316ccdf 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,6 +1,6 @@ import { action, runInAction } from "mobx"; import { Doc, Field } from "../../new_fields/Doc"; -import { Cast, StrCast } from "../../new_fields/Types"; +import { Cast, StrCast, ScriptCast } from "../../new_fields/Types"; import { URLField } from "../../new_fields/URLField"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; @@ -13,6 +13,7 @@ import { Docs } from "../documents/Documents"; import { ScriptField } from "../../new_fields/ScriptField"; import { List } from "../../new_fields/List"; import { PrefetchProxy } from "../../new_fields/Proxy"; +import { listSpec } from "../../new_fields/Schema"; export type dropActionType = "alias" | "copy" | undefined; export function SetupDrag( @@ -207,7 +208,6 @@ export namespace DragManager { } draggedDocuments: Doc[]; droppedDocuments: Doc[]; - removeDropProperties: string[] = []; offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; @@ -234,17 +234,18 @@ export namespace DragManager { export let StartDragFunctions: (() => void)[] = []; - export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + export async function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { runInAction(() => StartDragFunctions.map(func => func())); + await dragData.draggedDocuments.map(d => d.dragFactory); StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag : (dropData: { [id: string]: any }) => { - (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? - dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) : - dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? - dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) : - dragData.draggedDocuments + (dropData.droppedDocuments = + dragData.draggedDocuments.map(d => ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result : + dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) : + dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d) ); - dragData.removeDropProperties.map(prop => dropData.droppedDocuments.map((d: Doc) => d[prop] = undefined)); + dropData.droppedDocuments.forEach((drop: Doc, i: number) => + Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined)); }); } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index f3e1933d7..38fce4cf7 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -73,7 +73,6 @@ export default class KeyManager { SelectionManager.DeselectAll(); } } - main.toggleColorPicker(true); SelectionManager.DeselectAll(); DictationManager.Controls.stop(); SharingManager.Instance.close(); diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index debb941c8..55195b616 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -185,7 +185,22 @@ button:hover { overflow: auto; z-index: 1; } - +.mainContent { + width:100%; + height:100%; + position:absolute; +} +.mainView-flyoutContainer{ + display:flex; + flex-direction: column; + position: absolute; + width:100%; + height:100%; + border: black 1px solid; + .documentView-node-topmost { + background: lightgrey; + } +} #mainContent-div { width: 100%; height: 100%; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 18e5052b7..aac4af9f6 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,4 +1,4 @@ -import { IconName, library } from '@fortawesome/fontawesome-svg-core'; +import { library } from '@fortawesome/fontawesome-svg-core'; import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; @@ -6,60 +6,74 @@ import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; import Measure from 'react-measure'; -import { Doc, DocListCast, Field, FieldResult, HeightSym, Opt } from '../../new_fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, Opt } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; import { InkTool } from '../../new_fields/InkField'; import { List } from '../../new_fields/List'; +import { ObjectField } from '../../new_fields/ObjectField'; import { listSpec } from '../../new_fields/Schema'; import { ScriptField } from '../../new_fields/ScriptField'; -import { BoolCast, Cast, FieldValue, PromiseValue, StrCast, NumCast } from '../../new_fields/Types'; +import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse } from '../../Utils'; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; import { DictationManager } from '../util/DictationManager'; -import { SetupDrag, DragManager } from '../util/DragManager'; +import { DragManager } from '../util/DragManager'; import { HistoryUtil } from '../util/History'; import SharingManager from '../util/SharingManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView'; import { CollectionDockingView } from './collections/CollectionDockingView'; -import { CollectionTreeView } from './collections/CollectionTreeView'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import KeyManager from './GlobalKeyHandler'; import { InkingControl } from './InkingControl'; import "./Main.scss"; import MainViewModal from './MainViewModal'; +import { MainViewNotifs } from './MainViewNotifs'; import { DocumentView } from './nodes/DocumentView'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; -import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; -import { MainViewNotifs } from './MainViewNotifs'; -import { DocumentType } from '../documents/DocumentTypes'; @observer export class MainView extends React.Component { public static Instance: MainView; - @observable addMenuToggle = React.createRef(); - @observable public pwidth: number = 0; - @observable public pheight: number = 0; - private _buttonBarHeight = 85; - private dropDisposer?: DragManager.DragDropDisposer; - - @observable private dictationState = DictationManager.placeholder; - @observable private dictationSuccessState: boolean | undefined = undefined; - @observable private dictationDisplayState = false; - @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false; + private _buttonBarHeight = 75; + private _flyoutSizeOnDown = 0; + private _urlState: HistoryUtil.DocUrl; + private _dropDisposer?: DragManager.DragDropDisposer; + + @observable private _dictationState = DictationManager.placeholder; + @observable private _dictationSuccessState: boolean | undefined = undefined; + @observable private _dictationDisplayState = false; + @observable private _dictationListeningState: DictationManager.Controls.ListeningUIStatus = false; + + @observable private _panelWidth: number = 0; + @observable private _panelHeight: number = 0; + @observable private _flyoutTranslate: boolean = true; + @observable public addMenuToggle = React.createRef(); + @observable public flyoutWidth: number = 250; public hasActiveModal = false; - + public isPointerDown = false; public overlayTimeout: NodeJS.Timeout | undefined; + private set mainContainer(doc: Opt) { + if (doc) { + !("presentationView" in doc) && (doc.presentationView = new List([Docs.Create.TreeDocument([], { title: "Presentation" })])); + this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc); + } + } + + @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } + @computed private get userDoc() { return CurrentUserUtils.UserDocument; } + @computed public get mainFreeform(): Opt { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } + public initiateDictationFade = () => { let duration = DictationManager.Commands.dictationFadeDuration; this.overlayTimeout = setTimeout(() => { @@ -69,13 +83,6 @@ export class MainView extends React.Component { setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500); }, duration); } - - private urlState: HistoryUtil.DocUrl; - - @computed private get userDoc() { - return CurrentUserUtils.UserDocument; - } - public cancelDictationFade = () => { if (this.overlayTimeout) { clearTimeout(this.overlayTimeout); @@ -83,54 +90,15 @@ export class MainView extends React.Component { } } - @computed private get mainContainer(): Opt { - return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; - } - @computed get mainFreeform(): Opt { - let docs = DocListCast(this.mainContainer!.data); - return (docs && docs.length > 1) ? docs[1] : undefined; - } - public isPointerDown = false; - private set mainContainer(doc: Opt) { - if (doc) { - if (!("presentationView" in doc)) { - doc.presentationView = new List([Docs.Create.TreeDocument([], { title: "Presentation" })]); - } - this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc); - } - } - - @computed public get dictatedPhrase() { - return this.dictationState; - } - - public set dictatedPhrase(value: string) { - runInAction(() => this.dictationState = value); - } - - @computed public get dictationSuccess() { - return this.dictationSuccessState; - } - - public set dictationSuccess(value: boolean | undefined) { - runInAction(() => this.dictationSuccessState = value); - } - - @computed public get dictationOverlayVisible() { - return this.dictationDisplayState; - } + @computed public get dictatedPhrase() { return this._dictationState; } + @computed public get dictationSuccess() { return this._dictationSuccessState; } + @computed public get dictationOverlayVisible() { return this._dictationDisplayState; } + @computed public get isListening() { return this._dictationListeningState; } - public set dictationOverlayVisible(value: boolean) { - runInAction(() => this.dictationDisplayState = value); - } - - @computed public get isListening() { - return this.dictationListeningState; - } - - public set isListening(value: DictationManager.Controls.ListeningUIStatus) { - runInAction(() => this.dictationListeningState = value); - } + public set dictatedPhrase(value: string) { runInAction(() => this._dictationState = value); } + public set dictationSuccess(value: boolean | undefined) { runInAction(() => this._dictationSuccessState = value); } + public set dictationOverlayVisible(value: boolean) { runInAction(() => this._dictationDisplayState = value); } + public set isListening(value: DictationManager.Controls.ListeningUIStatus) { runInAction(() => this._dictationListeningState = value); } componentWillMount() { var tag = document.createElement('script'); @@ -147,13 +115,13 @@ export class MainView extends React.Component { //close presentation window.removeEventListener("pointerdown", this.globalPointerDown); window.removeEventListener("pointerup", this.globalPointerUp); - this.dropDisposer && this.dropDisposer(); + this._dropDisposer && this._dropDisposer(); } constructor(props: Readonly<{}>) { super(props); MainView.Instance = this; - this.urlState = HistoryUtil.parseUrl(window.location) || {} as any; + this._urlState = HistoryUtil.parseUrl(window.location) || {} as any; // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); if (window.location.pathname !== RouteStore.home) { @@ -233,7 +201,7 @@ export class MainView extends React.Component { { fireImmediately: true } ); } else { - if (received && this.urlState.sharing) { + if (received && this._urlState.sharing) { reaction( () => { let docking = CollectionDockingView.Instance; @@ -264,8 +232,8 @@ export class MainView extends React.Component { let freeformOptions: DocumentOptions = { x: 0, y: 400, - width: this.pwidth * .7, - height: this.pheight, + width: this._panelWidth * .7, + height: this._panelHeight, title: "My Blank Collection" }; let workspaces: FieldResult; @@ -293,7 +261,7 @@ export class MainView extends React.Component { openWorkspace = async (doc: Doc, fromHistory = false) => { CurrentUserUtils.MainDocId = doc[Id]; this.mainContainer = doc; - let state = this.urlState; + let state = this._urlState; if (state.sharing === true && !this.userDoc) { DocServer.Control.makeReadOnly(); } else { @@ -318,25 +286,21 @@ export class MainView extends React.Component { DocServer.Control.makeEditable(); } } - let col: Opt; // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) setTimeout(async () => { - if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) { - const l = Cast(col.data, listSpec(Doc)); - if (l) { - runInAction(() => MainViewNotifs.NotifsCol = col); - } - } + const col = this.userDoc && await Cast(this.userDoc.optionalRightCollection, Doc); + col && Cast(col.data, listSpec(Doc)) && runInAction(() => MainViewNotifs.NotifsCol = col); }, 100); return true; } drop = action((e: Event, de: DragManager.DropEvent) => { (de.data as DragManager.DocumentDragData).draggedDocuments.map(doc => { - if (doc.type !== DocumentType.DRAGBOX) { - let dbox = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); - dbox.factory = doc; - dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.factory, true)'); + if (!doc.onDragStart) { + let dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); + dbox.dragFactory = doc; + dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; + dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); doc = dbox; } Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc); @@ -351,30 +315,22 @@ export class MainView extends React.Component { @action onResize = (r: any) => { - this.pwidth = r.offset.width; - this.pheight = r.offset.height; - } - getPWidth = () => { - return this.pwidth; - } - getPHeight = () => { - return this.pheight; - } - getContentsHeight = () => { - return this.pheight - this._buttonBarHeight; + this._panelWidth = r.offset.width; + this._panelHeight = r.offset.height; } + getPWidth = () => this._panelWidth; + getPHeight = () => this._panelHeight; + getContentsHeight = () => this._panelHeight - this._buttonBarHeight; - @observable flyoutWidth: number = 250; - @observable flyoutTranslate: boolean = true; @computed get dockingContent() { - let flyoutWidth = this.flyoutWidth; - let countWidth = this.flyoutTranslate; - let mainCont = this.mainContainer; return {({ measureRef }) => -
    - {!mainCont ? (null) : - + {!this.mainContainer ? (null) : + ; } - _downsize = 0; onPointerDown = (e: React.PointerEvent) => { - this._downsize = e.clientX; + this._flyoutSizeOnDown = e.clientX; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -417,15 +372,15 @@ export class MainView extends React.Component { pointerOverDragger = () => { if (this.flyoutWidth === 0) { this.flyoutWidth = 250; - this.flyoutTranslate = false; + this._flyoutTranslate = false; } } @action pointerLeaveDragger = () => { - if (!this.flyoutTranslate) { + if (!this._flyoutTranslate) { this.flyoutWidth = 0; - this.flyoutTranslate = true; + this._flyoutTranslate = true; } } @@ -435,7 +390,7 @@ export class MainView extends React.Component { } @action onPointerUp = (e: PointerEvent) => { - if (Math.abs(e.clientX - this._downsize) < 4) { + if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4) { if (this.flyoutWidth < 5) this.flyoutWidth = 250; else this.flyoutWidth = 0; } @@ -461,8 +416,8 @@ export class MainView extends React.Component { return (null); } let libraryButtonDoc = Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc; - libraryButtonDoc.columnWidth = this.flyoutWidthFunc() / 3 - 30; - return
    + libraryButtonDoc.columnWidth = this.flyoutWidth / 3 - 30; + return
    + const sidebar = this.userDoc && this.userDoc.sidebarContainer; + return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( +
    {this.flyout} {this.expandButton} @@ -556,10 +508,10 @@ export class MainView extends React.Component { } @computed get expandButton() { - return !this.flyoutTranslate ? (
    { + return !this._flyoutTranslate ? (
    { runInAction(() => { this.flyoutWidth = 250; - this.flyoutTranslate = true; + this._flyoutTranslate = true; }); }}>
    ) : (null); } @@ -572,14 +524,6 @@ export class MainView extends React.Component { return { fontSize: "50%" }; } - onColorClick = (e: React.MouseEvent) => { - let target = (e.nativeEvent as any).path[0]; - let parent = (e.nativeEvent as any).path[1]; - if (target.localName === "input" || parent.localName === "span") { - e.stopPropagation(); - } - } - setWriteMode = (mode: DocServer.WriteMode) => { console.log(DocServer.WriteMode[mode]); const mode1 = mode; @@ -596,13 +540,12 @@ export class MainView extends React.Component { } protected createDropTarget = (ele: HTMLLabelElement) => { //used for stacking and masonry view - this.dropDisposer && this.dropDisposer(); + this._dropDisposer && this._dropDisposer(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } - @observable private _colorPickerDisplay = false; /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ nodesMenu() { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; @@ -610,14 +553,13 @@ export class MainView extends React.Component { // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); - return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} > + return < div id="add-nodes-menu" style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} > - +
    - {/*
  • */} @@ -658,18 +600,6 @@ export class MainView extends React.Component {
    ; } - - - @action - toggleColorPicker = (close = false) => { - this._colorPickerDisplay = close ? false : !this._colorPickerDisplay; - } - - @action.bound - toggleSearch = () => { - PromiseValue(Cast(CurrentUserUtils.UserDocument.Search, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - } - @computed private get dictationOverlay() { let success = this.dictationSuccess; let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`; diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index f08ea4891..3cf8c3eb3 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -70,7 +70,8 @@ export class ButtonBox extends DocComponent(Butt let missingParams = params && params.filter(p => this.props.Document[p] === undefined); params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... return ( -
    +
    {(this.Document.text || this.Document.title)} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f2a581c42..19ffdf0cd 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -16,7 +16,7 @@ import { AudioBox } from "./AudioBox"; import { ButtonBox } from "./ButtonBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; -import { DragBox } from "./DragBox"; +import { FontIconBox } from "./FontIconBox"; import { FieldView, FieldViewProps } from "./FieldView"; import { FormattedTextBox } from "./FormattedTextBox"; import { IconBox } from "./IconBox"; @@ -102,7 +102,7 @@ export class DocumentContentsView extends React.Component(Docu if (this._mainCont.current) { let dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.offset = this.Document.onDragStart ? [0, 0] : this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.moveDocument = this.props.moveDocument; + dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; - dragData.removeDropProperties = this.Document.removeDropProperties || []; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { dragComplete: action((emptyFunction)) }, - hideSource: !dropAction + hideSource: !dropAction && !this.Document.onDragStart }); } } @@ -246,7 +247,7 @@ export class DocumentView extends DocComponent(Docu this._hitTemplateDrag = true; } } - if (this.active && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + if ((this.active || this.Document.onDragStart) && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -258,9 +259,9 @@ export class DocumentView extends DocComponent(Docu if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && !this.topMost && e.buttons === 1) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart) && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); @@ -475,6 +476,12 @@ export class DocumentView extends DocComponent(Docu }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + let funcs: ContextMenuProps[] = []; + funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined }); + ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" }); @@ -627,8 +634,8 @@ export class DocumentView extends DocComponent(Docu this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); - const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; - const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; + const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); diff --git a/src/client/views/nodes/DragBox.scss b/src/client/views/nodes/DragBox.scss deleted file mode 100644 index fbb9b9c1c..000000000 --- a/src/client/views/nodes/DragBox.scss +++ /dev/null @@ -1,13 +0,0 @@ -.dragBox-outerDiv { - width: 100%; - height: 100%; - pointer-events: all; - border-radius: inherit; - background: black; - border-radius: 100%; - svg { - margin:18%; - width:65% !important; - height:65%; - } -} diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx deleted file mode 100644 index 1fd6cb5a5..000000000 --- a/src/client/views/nodes/DragBox.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit } from '@fortawesome/free-regular-svg-icons'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; -import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { emptyFunction } from '../../../Utils'; -import { CompileScript } from '../../util/Scripting'; -import { ContextMenu } from '../ContextMenu'; -import { DocComponent } from '../DocComponent'; -import { OverlayView } from '../OverlayView'; -import { ScriptBox } from '../ScriptBox'; -import { DocumentIconContainer } from './DocumentIcon'; -import './DragBox.scss'; -import { FieldView, FieldViewProps } from './FieldView'; -import { DragManager } from '../../util/DragManager'; -import { Docs } from '../../documents/Documents'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Cast } from '../../../new_fields/Types'; -import { ContextMenuProps } from '../ContextMenuItem'; - -library.add(faEdit as any); - -const DragSchema = createSchema({ - onDragStart: ScriptField, - text: "string" -}); - -type DragDocument = makeInterface<[typeof DragSchema]>; -const DragDocument = makeInterface(DragSchema); -@observer -export class DragBox extends DocComponent(DragDocument) { - _downX: number = 0; - _downY: number = 0; - public static LayoutString() { return FieldView.LayoutString(DragBox); } - _mainCont = React.createRef(); - onDragStart = (e: React.PointerEvent) => { - if (!e.ctrlKey && !e.altKey && !e.shiftKey && !this.props.isSelected() && e.button === 0) { - document.removeEventListener("pointermove", this.onDragMove); - document.addEventListener("pointermove", this.onDragMove); - document.removeEventListener("pointerup", this.onDragUp); - document.addEventListener("pointerup", this.onDragUp); - e.stopPropagation(); - e.preventDefault(); - } - } - - onDragMove = async (e: MouseEvent) => { - if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { - document.removeEventListener("pointermove", this.onDragMove); - document.removeEventListener("pointerup", this.onDragUp); - const onDragStart = this.Document.onDragStart; - e.stopPropagation(); - e.preventDefault(); - let dragData = new DragManager.DocumentDragData([this.props.Document]); - const factory = await Cast(this.props.Document.factory, Doc);// if there's a factory Doc that is being copied, make sure it's not pending. - dragData.removeDropProperties = factory ? Cast(factory.removeDropProperties, listSpec("string"), []) : []; - DragManager.StartDocumentDrag([this._mainCont.current!], dragData, e.clientX, e.clientY, { - finishDrag: async (dropData) => { - let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; - let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); - dropData.droppedDocuments = [doc]; - dragData.removeDropProperties.map(prop => dropData.droppedDocuments.map((d: Doc) => d[prop] = undefined)); - }, - handlers: { dragComplete: emptyFunction }, - hideSource: false - }); - } - e.stopPropagation(); - e.preventDefault(); - } - - onDragUp = (e: MouseEvent) => { - document.removeEventListener("pointermove", this.onDragMove); - document.removeEventListener("pointerup", this.onDragUp); - } - - onContextMenu = () => { - let funcs: ContextMenuProps[] = []; - funcs.push({ description: "Set Make Aliases", icon: "edit", event: () => this.props.Document.factory && (this.props.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.factory)')) }); - funcs.push({ description: "Set Make Copies", icon: "edit", event: () => this.props.Document.factory && (this.props.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.factory, true)')) }); - funcs.push({ - description: "Edit OnClick script", icon: "edit", event: () => { - let overlayDisposer: () => void = emptyFunction; - const script = this.Document.onDragStart; - let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = overlayDisposer()} onSave={(text, onError) => { - const script = CompileScript(text, { - params: { this: Doc.name }, - typecheck: false, - editable: true, - transformer: DocumentIconContainer.getTransformer() - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } - this.Document.onClick = new ScriptField(script); - overlayDisposer(); - }} showDocumentIcons />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` }); - } - }); - ContextMenu.Instance.addItem({ description: "DragBox Funcs...", subitems: funcs, icon: "asterisk" }); - } - - render() { - return (
    - -
    ); - } -} \ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss new file mode 100644 index 000000000..75d093fcb --- /dev/null +++ b/src/client/views/nodes/FontIconBox.scss @@ -0,0 +1,13 @@ +.fontIconBox-outerDiv { + width: 100%; + height: 100%; + pointer-events: all; + border-radius: inherit; + background: black; + border-radius: 100%; + svg { + margin:18%; + width:65% !important; + height:65%; + } +} diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx new file mode 100644 index 000000000..3b580d851 --- /dev/null +++ b/src/client/views/nodes/FontIconBox.tsx @@ -0,0 +1,21 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { DocComponent } from '../DocComponent'; +import './FontIconBox.scss'; +import { FieldView, FieldViewProps } from './FieldView'; +const FontIconSchema = createSchema({ + icon: "string" +}); + +type FontIconDocument = makeInterface<[typeof FontIconSchema]>; +const FontIconDocument = makeInterface(FontIconSchema); +@observer +export class FontIconBox extends DocComponent(FontIconDocument) { + public static LayoutString() { return FieldView.LayoutString(FontIconBox); } + + render() { + return
    ; + } +} \ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 74a41d8cf..9f9f79dc4 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -99,9 +99,9 @@ export class CurrentUserUtils { } if (doc.Library === undefined) { - let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, title: "Search" }); - let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, title: "Library" }); - let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, title: "Create" }); + let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search" }); + let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library" }); + let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create" }); if (doc.sidebarContainer === undefined) { doc.sidebarContainer = new Doc(); (doc.sidebarContainer as Doc).chromeStatus = "disabled"; @@ -114,40 +114,39 @@ export class CurrentUserUtils { library.xMargin = 5; library.yMargin = 5; library.dropAction = "alias"; - library.boxShadow = "1 1 3"; Library.targetContainer = doc.sidebarContainer; Library.library = library; Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library"); - const searchBox = Docs.Create.QueryDocument({ title: "Searching" }); - searchBox.boxShadow = "0 0"; + const searchBox = Docs.Create.QueryDocument({ title: "search stack" }); searchBox.ignoreClick = true; Search.searchBox = searchBox; Search.targetContainer = doc.sidebarContainer; Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox"); - let createCollection = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" }); - let createWebPage = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); + let createCollection = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" }); + createCollection.onDragStart = ScriptField.MakeFunction('Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })'); + let createWebPage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })'); - let createCatImage = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); + let createCatImage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })'); - let createButton = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); + let createButton = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })'); - let createPresentation = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); + let createPresentation = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })'); - let createFolderImport = Docs.Create.DragboxDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); + let createFolderImport = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); - const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, chromeStatus: "disabled", title: "buttons" }); + const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" }); const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 }); color.dropAction = "alias"; color.ignoreClick = true; color.removeDropProperties = new List(["dropAction", "ignoreClick"]); - const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "buttons" }); + const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" }); Create.targetContainer = doc.sidebarContainer; Create.creators = creators; Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); - const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "buttons" }); + const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "library stack" }); libraryButtons.sectionFilter = "title"; libraryButtons.boxShadow = "0 0"; libraryButtons.ignoreClick = true; -- cgit v1.2.3-70-g09d2 From baf6ed901d341cade58741d363bbc475519558ae Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sun, 13 Oct 2019 03:10:29 -0400 Subject: made CollectionLinearView out of doc buttons --- src/client/views/CollectionLinearView.scss | 13 ++- src/client/views/CollectionLinearView.tsx | 111 +++++++++++---------- src/client/views/MainView.tsx | 34 ++++++- .../views/collections/CollectionBaseView.tsx | 6 +- src/client/views/collections/CollectionSubView.tsx | 1 + src/client/views/collections/CollectionView.tsx | 2 + .../views/collections/CollectionViewChromes.tsx | 1 + .../authentication/models/current_user_utils.ts | 2 + 8 files changed, 111 insertions(+), 59 deletions(-) (limited to 'src/server') diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss index 30be07a9f..46a226eef 100644 --- a/src/client/views/CollectionLinearView.scss +++ b/src/client/views/CollectionLinearView.scss @@ -1,8 +1,13 @@ @import "globalCssVariables"; @import "nodeModuleOverrides"; +.collectionLinearView-outer{ + overflow: hidden; + height:100%; + padding:5px; +} .collectionLinearView { - + display:flex; >label { background: $dark-color; color: $light-color; @@ -18,6 +23,8 @@ label p { padding-left: 10.5px; + width: 500px; + height: 500px; } label:hover { @@ -41,18 +48,14 @@ .collectionLinearView-content { display: flex; opacity: 1; - margin: 0; padding: 0; position: relative; - float: right; .collectionFreeFormDocumentView-container { position: relative; } .collectionLinearView-docBtn { position:relative; margin-right: 10px; - width: 35px; - height: 35px; } .collectionLinearView-round-button { width: 36px; diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 18e3598a5..692f940f8 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -7,22 +7,23 @@ import { InkTool } from '../../new_fields/InkField'; import { ObjectField } from '../../new_fields/ObjectField'; import { ScriptField } from '../../new_fields/ScriptField'; import { NumCast, StrCast } from '../../new_fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, returnFalse } from '../../Utils'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, returnFalse, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; import { InkingControl } from './InkingControl'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, documentSchema } from './nodes/DocumentView'; import "./CollectionLinearView.scss"; +import { makeInterface } from '../../new_fields/Schema'; +import { CollectionSubView } from './collections/CollectionSubView'; -interface CollectionLinearViewProps { - Document: Doc; - fieldKey: string; -} + +type LinearDocument = makeInterface<[typeof documentSchema,]>; +const LinearDocument = makeInterface(documentSchema); @observer -export class CollectionLinearView extends React.Component{ +export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; @@ -30,7 +31,7 @@ export class CollectionLinearView extends React.Component { //used for stacking and masonry view + protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this._dropDisposer && this._dropDisposer(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); @@ -38,16 +39,18 @@ export class CollectionLinearView extends React.Component { - (de.data as DragManager.DocumentDragData).draggedDocuments.map(doc => { - if (!doc.onDragStart) { - let dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); + (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { + let dbox = doc; + if (!doc.onDragStart && this.props.Document.convertToButtons) { + dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); dbox.dragFactory = doc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); - doc = dbox; } - Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc); + (de.data as DragManager.DocumentDragData).droppedDocuments[i] = dbox; }); + e.stopPropagation(); + return super.drop(e, de); }); selected = (tool: InkTool) => { @@ -58,49 +61,55 @@ export class CollectionLinearView extends React.Component NumCast(this.props.Document.height) - 5; render() { - return
    - - + let guid = Utils.GenerateGuid(); + return
    + +
    - - + {this.props.showHiddenControls ? : (null)} + {this.props.showHiddenControls ? : (null)} - {DocListCast(this.props.Document[this.props.fieldKey]).map(doc =>
    - Doc.RemoveDocFromList(this.props.Document, this.props.fieldKey, doc)} - ruleProvider={undefined} - onClick={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={() => 35 / NumCast(doc.nativeWidth, 35)} - PanelWidth={() => 35} - PanelHeight={() => 35} - renderDepth={0} - focus={emptyFunction} - backgroundColor={returnEmptyString} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - -
    )} + {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => +
    + this.dimension() / (10 + NumCast(pair.layout.nativeWidth, this.dimension()))} // ugh - need to get rid of this inline function to avoid recomputing + PanelWidth={this.dimension} + PanelHeight={this.dimension} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne}> + +
    )} {/*
  • */} - - - - - - + {this.props.showHiddenControls ? <> + + + + + + : (null)}
    -
    ; +
    ; } } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 4367785b6..5756c1510 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -441,10 +441,42 @@ export class MainView extends React.Component { }}>
    ) : (null); } + addButtonDoc = (doc: Doc) => { + Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc); + return true; + } + remButtonDoc = (doc: Doc) => { + Doc.RemoveDocFromList(CurrentUserUtils.UserDocument, "docButtons", doc); + return true; + } @computed get docButtons() { return
    - +
    ; } diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 61919427a..7798964ea 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -23,6 +23,7 @@ export enum CollectionViewType { Stacking, Masonry, Pivot, + Linear, } export namespace CollectionViewType { @@ -35,7 +36,8 @@ export namespace CollectionViewType { ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], ["masonry", CollectionViewType.Masonry], - ["pivot", CollectionViewType.Pivot] + ["pivot", CollectionViewType.Pivot], + ["linear", CollectionViewType.Linear] ]); export const valueOf = (value: string) => { @@ -177,7 +179,7 @@ export class CollectionBaseView extends React.Component {
    diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index fdbe5339d..6e8e4fa12 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -33,6 +33,7 @@ export interface CollectionViewProps extends FieldViewProps { VisibleHeight?: () => number; chromeCollapsed: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + showHiddenControls?: boolean; // hack for showing the undo/redo/ink controls in a linear view -- needs to be redone } export interface SubCollectionViewProps extends CollectionViewProps { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 893763840..3d5b4e562 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -19,6 +19,7 @@ import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; +import { CollectionLinearView } from '../CollectionLinearView'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -66,6 +67,7 @@ export class CollectionView extends React.Component { case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (); } case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (); } + case CollectionViewType.Linear: { return (); } case CollectionViewType.Freeform: default: this.props.Document.freeformLayoutEngine = undefined; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 72f3514b6..3a66c05f4 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -399,6 +399,7 @@ export class CollectionViewBaseChrome extends React.ComponentStacking View +
    Date: Sun, 13 Oct 2019 19:17:45 -0400 Subject: minor cleanups --- src/client/util/TooltipTextMenu.tsx | 2 +- src/client/util/UndoManager.ts | 4 +- src/client/views/CollectionLinearView.scss | 118 ++++++++++----------- src/client/views/CollectionLinearView.tsx | 103 ++++++++++-------- src/client/views/DocumentDecorations.tsx | 8 +- src/client/views/MainView.tsx | 3 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FontIconBox.tsx | 2 +- src/new_fields/Doc.ts | 5 +- .../authentication/models/current_user_utils.ts | 21 +++- 10 files changed, 150 insertions(+), 118 deletions(-) (limited to 'src/server') diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index c82d3bc63..31d98887f 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -850,7 +850,7 @@ export class TooltipTextMenu { } this.view = view; let state = view.state; - DocumentDecorations.Instance.TextBar && DocumentDecorations.Instance.setTextBar(DocumentDecorations.Instance.TextBar); + DocumentDecorations.Instance.showTextBar(); props && (this.editorProps = props); // Don't do anything if the document/selection didn't change if (lastState && lastState.doc.eq(state.doc) && diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 7abb9d1ee..472afac1d 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -73,8 +73,8 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; - let undoStack: UndoBatch[] = observable([]); - let redoStack: UndoBatch[] = observable([]); + export let undoStack: UndoBatch[] = observable([]); + export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; let batchCounter = 0; let undoing = false; diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss index 46a226eef..1e6bb5922 100644 --- a/src/client/views/CollectionLinearView.scss +++ b/src/client/views/CollectionLinearView.scss @@ -5,73 +5,69 @@ overflow: hidden; height:100%; padding:5px; -} -.collectionLinearView { - display:flex; - >label { - background: $dark-color; - color: $light-color; - display: inline-block; - border-radius: 18px; - font-size: 25px; - width: 36px; - height: 36px; - margin-right: 10px; - cursor: pointer; - transition: transform 0.2s; - } + .collectionLinearView { + display:flex; + >label { + background: $dark-color; + color: $light-color; + display: inline-block; + border-radius: 18px; + font-size: 25px; + width: 36px; + height: 36px; + margin-right: 10px; + cursor: pointer; + transition: transform 0.2s; + } - label p { - padding-left: 10.5px; - width: 500px; - height: 500px; - } + label p { + padding-left: 10.5px; + width: 500px; + height: 500px; + } - label:hover { - background: $main-accent; - transform: scale(1.15); - } + label:hover { + background: $main-accent; + transform: scale(1.15); + } - >input { - display: none; - } - >input:not(:checked)~.collectionLinearView-content { - display: none; - } + >input { + display: none; + } + >input:not(:checked)~.collectionLinearView-content { + display: none; + } - >input:checked~label { - transform: rotate(45deg); - transition: transform 0.5s; - cursor: pointer; - } + >input:checked~label { + transform: rotate(45deg); + transition: transform 0.5s; + cursor: pointer; + } - .collectionLinearView-content { - display: flex; - opacity: 1; - padding: 0; - position: relative; - .collectionFreeFormDocumentView-container { + .collectionLinearView-content { + display: flex; + opacity: 1; + padding: 0; position: relative; - } - .collectionLinearView-docBtn { - position:relative; - margin-right: 10px; - } - .collectionLinearView-round-button { - width: 36px; - height: 36px; - border-radius: 18px; - font-size: 15px; - } - - .collectionLinearView-round-button:hover { - transform: scale(1.15); + + .collectionLinearView-docBtn { + position:relative; + margin-right: 10px; + } + .collectionLinearView-docBtn:hover { + transform: scale(1.15); + } + + .collectionLinearView-round-button { + width: 36px; + height: 36px; + border-radius: 18px; + font-size: 15px; + } + + .collectionLinearView-round-button:hover { + transform: scale(1.15); + } } - - } - - .collectionLinearView-add-button { - position: relative; - margin-right: 10px; } } diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 692f940f8..49c67a448 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -1,8 +1,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, computed } from 'mobx'; +import { action, observable, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; +import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc'; import { InkTool } from '../../new_fields/InkField'; import { ObjectField } from '../../new_fields/ObjectField'; import { ScriptField } from '../../new_fields/ScriptField'; @@ -25,12 +25,26 @@ const LinearDocument = makeInterface(documentSchema); @observer export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef(); + @observable private _checked = false; private _dropDisposer?: DragManager.DragDropDisposer; + private _heightDisposer?: IReactionDisposer; componentWillUnmount() { this._dropDisposer && this._dropDisposer(); + this._heightDisposer && this._heightDisposer(); } + componentDidMount() { + // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). + this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this._checked ? 1 : 0), + () => { + if (true || this.props.Document.fitWidth) { + this.props.Document.width = 36 + (this._checked ? this.childDocs.length * (this.props.Document[HeightSym]() + 10) : 10); + } + }, + { fireImmediately: true } + ); + } protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this._dropDisposer && this._dropDisposer(); if (ele) { @@ -66,50 +80,49 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { dimension = () => NumCast(this.props.Document.height) - 5; render() { let guid = Utils.GenerateGuid(); - return
    - - - -
    - {this.props.showHiddenControls ? : (null)} - {this.props.showHiddenControls ? : (null)} + return
    +
    + this._checked = this.addMenuToggle.current!.checked)} /> + - {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => -
    - this.dimension() / (10 + NumCast(pair.layout.nativeWidth, this.dimension()))} // ugh - need to get rid of this inline function to avoid recomputing - PanelWidth={this.dimension} - PanelHeight={this.dimension} - renderDepth={this.props.renderDepth + 1} - focus={emptyFunction} - backgroundColor={returnEmptyString} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - -
    )} - {/*
  • */} - {this.props.showHiddenControls ? <> - - - - - - : (null)} +
    + {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => +
    + this.dimension() / (10 + NumCast(pair.layout.nativeWidth, this.dimension()))} // ugh - need to get rid of this inline function to avoid recomputing + PanelWidth={this.dimension} + PanelHeight={this.dimension} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne}> + +
    )} + {/*
  • */} + {this.props.showHiddenControls ? <> + + + + + + : (null)} +
    -
    ; +
    ; } } \ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 1d9f0c74b..3d73f048d 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -547,10 +547,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } TextBar: HTMLDivElement | undefined; - setTextBar = (ele: HTMLDivElement) => { + private setTextBar = (ele: HTMLDivElement) => { if (ele) { this.TextBar = ele; - TooltipTextMenu.Toolbar && Array.from(ele.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && ele.appendChild(TooltipTextMenu.Toolbar); + } + } + public showTextBar = () => { + if (this.TextBar) { + TooltipTextMenu.Toolbar && Array.from(this.TextBar.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && this.TextBar.appendChild(TooltipTextMenu.Toolbar); } } render() { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 5756c1510..39585113b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -334,6 +334,7 @@ export class MainView extends React.Component { return CollectionDockingView.AddRightSplit(doc, undefined); } } + mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1); @computed get flyout() { let sidebarContent = this.userDoc && this.userDoc.sidebarContainer; @@ -379,7 +380,7 @@ export class MainView extends React.Component { removeDocument={returnFalse} ruleProvider={undefined} onClick={undefined} - ScreenToLocalTransform={Transform.Identity} + ScreenToLocalTransform={this.mainContainerXf} ContentScaling={returnOne} PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getContentsHeight} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7334de92c..188c18ba7 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -185,7 +185,7 @@ export class DocumentView extends DocComponent(Docu (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { e.stopPropagation(); let preventDefault = true; - if (this._doubleTap && this.props.renderDepth) { + if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !this.onClickHandler.script)) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click let fullScreenAlias = Doc.MakeAlias(this.props.Document); let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 3b580d851..3f5afb6d1 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -16,6 +16,6 @@ export class FontIconBox extends DocComponent( public static LayoutString() { return FieldView.LayoutString(FontIconBox); } render() { - return
    ; + return ; } } \ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 66036f673..4bed113e3 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -14,6 +14,7 @@ import { ComputedField } from "./ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types"; import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; import { intersectRect } from "../Utils"; +import { UndoManager } from "../client/util/UndoManager"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -733,4 +734,6 @@ Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return Doc.MakeCopy(doc, copyProto); }); Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); Scripting.addGlobal(function aliasDocs(field: any) { return new List(field.map((d: any) => Doc.MakeAlias(d))); }); -Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); \ No newline at end of file +Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); +Scripting.addGlobal(function undo() { return UndoManager.Undo(); }); +Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); \ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 3858907ba..73cac879e 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -1,4 +1,4 @@ -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable, runInAction, reaction } from "mobx"; import * as rp from 'request-promise'; import { DocServer } from "../../../client/DocServer"; import { Docs } from "../../../client/documents/Documents"; @@ -13,6 +13,8 @@ import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; import { RouteStore } from "../../RouteStore"; import { ScriptField } from "../../../new_fields/ScriptField"; +import { ButtonBox } from "../../../client/views/nodes/ButtonBox"; +import { UndoManager } from "../../../client/util/UndoManager"; export class CurrentUserUtils { private static curr_id: string; @@ -32,7 +34,6 @@ export class CurrentUserUtils { doc.viewType = CollectionViewType.Tree; doc.layout = CollectionView.LayoutString(); doc.title = Doc.CurrentUserEmail; - this.updateUserDocument(doc); doc.data = new List(); doc.gridGap = 5; doc.xMargin = 5; @@ -41,11 +42,21 @@ export class CurrentUserUtils { doc.boxShadow = "0 0"; doc.convertToButtons = true; // for CollectionLinearView used as the docButton layout doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" }); - return doc; + return this.updateUserDocument(doc);// this should be the last } static updateUserDocument(doc: Doc) { + if (doc.undoBtn === undefined) { + doc.undoBtn = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "undo-alt" }); + (doc.undoBtn as Doc).onClick = ScriptField.MakeScript('undo()'); + Doc.AddDocToList(doc, "docButtons", doc.undoBtn as Doc); + } + if (doc.redoBtn === undefined) { + doc.redoBtn = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "redo-alt" }); + (doc.redoBtn as Doc).onClick = ScriptField.MakeScript('redo()'); + Doc.AddDocToList(doc, "docButtons", doc.redoBtn as Doc); + } // setup workspaces library item if (doc.workspaces === undefined) { const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 }); @@ -181,6 +192,10 @@ export class CurrentUserUtils { doc.preventTreeViewOpen = true; doc.forceActive = true; doc.lockedPosition = true; + doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => (doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => (doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); + + return doc; } public static loadCurrentUser() { -- cgit v1.2.3-70-g09d2 From fb3d1a66100e9ecf573031cfcafeb983f4ad5d5b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 14 Oct 2019 13:36:15 -0400 Subject: beginning server refactor --- src/server/RouteSubscriber.ts | 26 ++++ src/server/index.ts | 341 +++++++++++++++++++++++++----------------- 2 files changed, 228 insertions(+), 139 deletions(-) create mode 100644 src/server/RouteSubscriber.ts (limited to 'src/server') diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts new file mode 100644 index 000000000..e49be8af5 --- /dev/null +++ b/src/server/RouteSubscriber.ts @@ -0,0 +1,26 @@ +export default class RouteSubscriber { + private _root: string; + private requestParameters: string[] = []; + + constructor(root: string) { + this._root = root; + } + + add(...parameters: string[]) { + this.requestParameters.push(...parameters); + return this; + } + + public get root() { + return this._root; + } + + public get build() { + let output = this._root; + if (this.requestParameters.length) { + output = `${output}/:${this.requestParameters.join("/:")}`; + } + return output; + } + +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 62938b9c7..fbc22d665 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -55,6 +55,7 @@ import { ParsedPDF } from "./PdfTypes"; import { reject } from 'bluebird'; import { ExifData } from 'exif'; import { Result } from '../client/northstar/model/idea/idea'; +import RouteSubscriber from './RouteSubscriber'; const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -106,6 +107,26 @@ enum Method { POST } +export type ValidationHandler = (user: DashUserModel, req: express.Request, res: express.Response) => any | Promise; +export type RejectionHandler = (req: express.Request, res: express.Response) => any | Promise; +export type ErrorHandler = (req: express.Request, res: express.Response, error: any) => any | Promise; + +const LoginRedirect: RejectionHandler = (_req, res) => res.redirect(RouteStore.login); + +export interface RouteInitializer { + method: Method; + subscribers: string | RouteSubscriber | (string | RouteSubscriber)[]; + onValidation: ValidationHandler; + onRejection?: RejectionHandler; + onError?: ErrorHandler; +} + +const isSharedDocAccess = (target: string) => { + const shared = qs.parse(qs.extract(target), { sort: false }).sharing === "true"; + const docAccess = target.startsWith("/doc/"); + return shared && docAccess; +}; + /** * Please invoke this function when adding a new route to Dash's server. * It ensures that any requests leading to or containing user-sensitive information @@ -115,22 +136,40 @@ enum Method { * @param onRejection an optional callback invoked on return if no user is found to be logged in * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler */ -function addSecureRoute(method: Method, - handler: (user: DashUserModel, res: express.Response, req: express.Request) => void, - onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login), - ...subscribers: string[] -) { - let abstracted = (req: express.Request, res: express.Response) => { - let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true"; - sharing = sharing && req.originalUrl.startsWith("/doc/"); - if (req.user || sharing) { - handler(req.user as any, res, req); +function addSecureRoute(initializer: RouteInitializer) { + const { method, subscribers, onValidation, onRejection, onError } = initializer; + let abstracted = async (req: express.Request, res: express.Response) => { + const { user, originalUrl: target } = req; + if (user || isSharedDocAccess(target)) { + try { + await onValidation(user, req, res); + } catch (e) { + if (onError) { + onError(req, res, e); + } else { + _error(res, `The server encountered an internal error handling ${target}.`, e); + } + } } else { - req.session!.target = req.originalUrl; - onRejection(res, req); + req.session!.target = target; + try { + await (onRejection || LoginRedirect)(req, res); + } catch (e) { + if (onError) { + onError(req, res, e); + } else { + _error(res, `The server encountered an internal error when rejecting ${target}.`, e); + } + } } }; - subscribers.forEach(route => { + const subscribe = (subscriber: RouteSubscriber | string) => { + let route: string; + if (typeof subscriber === "string") { + route = subscriber; + } else { + route = subscriber.build; + } switch (method) { case Method.GET: app.get(route, abstracted); @@ -139,7 +178,12 @@ function addSecureRoute(method: Method, app.post(route, abstracted); break; } - }); + }; + if (Array.isArray(subscribers)) { + subscribers.forEach(subscribe); + } else { + subscribe(subscribers); + } } // STATIC FILE SERVING @@ -324,13 +368,17 @@ app.get("/serializeDoc/:docId", async (req, res) => { export type Hierarchy = { [id: string]: string | Hierarchy }; export type ZipMutator = (file: Archiver.Archiver) => void | Promise; -app.get(`${RouteStore.imageHierarchyExport}/:docId`, async (req, res) => { - const id = req.params.docId; - const hierarchy: Hierarchy = {}; - await targetedVisitorRecursive(id, hierarchy); - BuildAndDispatchZip(res, async zip => { - await hierarchyTraverserRecursive(zip, hierarchy); - }); +addSecureRoute({ + method: Method.GET, + subscribers: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'), + onValidation: async (_user, req, res) => { + const id = req.params.docId; + const hierarchy: Hierarchy = {}; + await targetedVisitorRecursive(id, hierarchy); + BuildAndDispatchZip(res, async zip => { + await hierarchyTraverserRecursive(zip, hierarchy); + }); + } }); const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise => { @@ -577,50 +625,49 @@ function LoadPage(file: string, pageNumber: number, res: Response) { }); } -// anyone attempting to navigate to localhost at this port will -// first have to login -addSecureRoute( - Method.GET, - (user, res) => res.redirect(RouteStore.home), - undefined, - RouteStore.root -); - -addSecureRoute( - Method.GET, - async (_, res) => { +/** + * Anyone attempting to navigate to localhost at this port will + * first have to log in. + */ +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.root, + onValidation: (_user, _req, res) => res.redirect(RouteStore.home) +}); + +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.getUsers, + onValidation: async (_user, _req, res) => { const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users"); const results = await cursor.toArray(); res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId }))); }, - undefined, - RouteStore.getUsers -); +}); -addSecureRoute( - Method.GET, - (user, res, req) => { +addSecureRoute({ + method: Method.GET, + subscribers: [RouteStore.home, RouteStore.openDocumentWithId], + onValidation: (_user, req, res) => { let detector = new mobileDetect(req.headers['user-agent'] || ""); let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; res.sendFile(path.join(__dirname, '../../deploy/' + filename)); }, - undefined, - RouteStore.home, RouteStore.openDocumentWithId -); - -addSecureRoute( - Method.GET, - (user, res) => res.send(user.userDocumentId), - (res) => res.send(undefined), - RouteStore.getUserDocumentId, -); - -addSecureRoute( - Method.GET, - (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); }, - (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })), - RouteStore.getCurrUser -); +}); + +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.getUserDocumentId, + onValidation: (user, _req, res) => res.send(user.userDocumentId), + onRejection: (_req, res) => res.send(undefined) +}); + +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.getCurrUser, + onValidation: (user, _req, res) => { res.send(JSON.stringify(user)); }, + onRejection: (_req, res) => res.send(JSON.stringify({ id: "__guest__", email: "" })) +}); const ServicesApiKeyMap = new Map([ ["face", process.env.FACE], @@ -628,10 +675,14 @@ const ServicesApiKeyMap = new Map([ ["handwriting", process.env.HANDWRITING] ]); -addSecureRoute(Method.GET, (user, res, req) => { - let service = req.params.requestedservice; - res.send(ServicesApiKeyMap.get(service)); -}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`); +addSecureRoute({ + method: Method.GET, + subscribers: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'), + onValidation: (_user, req, res) => { + let service = req.params.requestedservice; + res.send(ServicesApiKeyMap.get(service)); + } +}); class NodeCanvasFactory { create = (width: number, height: number) => { @@ -669,10 +720,10 @@ interface ImageFileResponse { exif: Opt; } -// SETTERS -app.post( - RouteStore.upload, - (req, res) => { +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.upload, + onValidation: (_user, req, res) => { let form = new formidable.IncomingForm(); form.uploadDir = uploadDirectory; form.keepExtensions = true; @@ -705,20 +756,25 @@ app.post( _success(res, results); }); } -); +}); -app.post(RouteStore.inspectImage, async (req, res) => { - const { source } = req.body; - if (typeof source === "string") { - const uploadInformation = await DashUploadUtils.UploadImage(source); - return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0])); +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.inspectImage, + onValidation: async (_user, req, res) => { + const { source } = req.body; + if (typeof source === "string") { + const uploadInformation = await DashUploadUtils.UploadImage(source); + return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0])); + } + res.send({}); } - res.send({}); }); -addSecureRoute( - Method.POST, - (user, res, req) => { +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.dataUriToImage, + onValidation: (_user, req, res) => { const uri = req.body.uri; const filename = req.body.name; if (!uri || !filename) { @@ -751,10 +807,9 @@ addSecureRoute( } res.send("/files/" + filename + ext); }); - }, - undefined, - RouteStore.dataUriToImage -); + } +}); + // AUTHENTICATION // Sign Up @@ -793,29 +848,27 @@ app.use(RouteStore.corsProxy, (req, res) => { }).pipe(res); }); -addSecureRoute( - Method.GET, - (user, res, req) => { +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.delete, + onValidation: (_user, _req, res) => { if (release) { return _permission_denied(res, deletionPermissionError); } deleteFields().then(() => res.redirect(RouteStore.home)); - }, - undefined, - RouteStore.delete -); + } +}); -addSecureRoute( - Method.GET, - (_user, res, _req) => { +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.deleteAll, + onValidation: (_user, _req, res) => { if (release) { return _permission_denied(res, deletionPermissionError); } deleteAll().then(() => res.redirect(RouteStore.home)); - }, - undefined, - RouteStore.deleteAll -); + } +}); app.use(wdm(compiler, { publicPath: config.output.publicPath })); @@ -946,21 +999,28 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.readGoogleAccessToken, async (req, res) => { - const userId = req.header("userId")!; - const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); - const information = { credentialsPath, userId }; - if (!token) { - return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); +addSecureRoute({ + method: Method.GET, + subscribers: RouteStore.readGoogleAccessToken, + onValidation: async (user, _req, res) => { + const userId = user.id; + const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + const information = { credentialsPath, userId }; + if (!token) { + return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + } + GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); } - GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); }); -app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { - const userId = req.header("userId")!; - const information = { credentialsPath, userId }; - const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); - res.send(token.access_token); +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.writeGoogleAccessToken, + onValidation: async (user, req, res) => { + const information = { credentialsPath, userId: user.id }; + const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); + res.send(token.access_token); + } }); const tokenError = "Unable to successfully upload bytes for all images!"; @@ -974,47 +1034,50 @@ export interface NewMediaItem { }; } -app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { - const { media } = req.body; - const userId = req.header("userId"); - - if (!userId) { - return _error(res, userIdError); - } - - await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); - - let failed: number[] = []; +addSecureRoute({ + method: Method.POST, + subscribers: RouteStore.googlePhotosMediaUpload, + onValidation: async (user, req, res) => { + const { media } = req.body; + const userId = user.id; + if (!userId) { + return _error(res, userIdError); + } - const newMediaItems = await BatchedArray.from(media, { batchSize: 25 }).batchedMapPatientInterval( - { magnitude: 100, unit: TimeUnit.Milliseconds }, - async (batch: GooglePhotosUploadUtils.MediaInput[]) => { - const newMediaItems: NewMediaItem[] = []; - for (let index = 0; index < batch.length; index++) { - const element = batch[index]; - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); - if (!uploadToken) { - failed.push(index); - } else { - newMediaItems.push({ - description: element.description, - simpleMediaItem: { uploadToken } - }); + await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); + + let failed: number[] = []; + + const newMediaItems = await BatchedArray.from(media, { batchSize: 25 }).batchedMapPatientInterval( + { magnitude: 100, unit: TimeUnit.Milliseconds }, + async (batch: GooglePhotosUploadUtils.MediaInput[]) => { + const newMediaItems: NewMediaItem[] = []; + for (let index = 0; index < batch.length; index++) { + const element = batch[index]; + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); + if (!uploadToken) { + failed.push(index); + } else { + newMediaItems.push({ + description: element.description, + simpleMediaItem: { uploadToken } + }); + } } + return newMediaItems; } - return newMediaItems; + ); + + const failedCount = failed.length; + if (failedCount) { + console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } - ); - const failedCount = failed.length; - if (failedCount) { - console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); + GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( + result => _success(res, { results: result.newMediaItemResults, failed }), + error => _error(res, mediaError, error) + ); } - - GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - result => _success(res, { results: result.newMediaItemResults, failed }), - error => _error(res, mediaError, error) - ); }); interface MediaItem { -- cgit v1.2.3-70-g09d2 From e8e713173e93eaa9fc2d2865d5efabdcca51b1f3 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Mon, 14 Oct 2019 19:00:40 -0400 Subject: fixed some minor layout issues with flyout and masonry views --- src/client/views/MainView.scss | 5 ++--- src/client/views/MainView.tsx | 10 ++++------ src/client/views/collections/CollectionStackingView.tsx | 4 ++-- src/server/index.ts | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) (limited to 'src/server') diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index e61494e71..21b135c49 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -33,7 +33,6 @@ position: absolute; width:100%; height:100%; - border: black 1px solid; .documentView-node-topmost { background: lightgrey; } @@ -63,8 +62,8 @@ .mainView-expandFlyoutButton { position: absolute; - top: 30px; - right: 30px; + top: 5px; + right: 5px; cursor: pointer; } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 209612a96..ddea3e223 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -436,12 +436,10 @@ export class MainView extends React.Component { } @computed get expandButton() { - return !this._flyoutTranslate ? (
    { - runInAction(() => { - this.flyoutWidth = 250; - this._flyoutTranslate = true; - }); - }}>
    ) : (null); + return !this._flyoutTranslate ? (
    { + this.flyoutWidth = 250; + this._flyoutTranslate = true; + })}>
    ) : (null); } addButtonDoc = (doc: Doc) => { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index a0dbeeb93..90fc9c3e0 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -49,7 +49,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); } - @computed get NodeWidth() { return this.props.PanelWidth(); } + @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } @@ -119,7 +119,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)), 0); } else { let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); - return this.props.ContentScaling() * (sum + (this.Sections.size ? 85 : -22)); + return this.props.ContentScaling() * (sum + (this.Sections.size ? 85 : -15)); } } return -1; diff --git a/src/server/index.ts b/src/server/index.ts index 2203ae2e1..c1dba2976 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -142,7 +142,7 @@ function addSecureRoute(initializer: RouteInitializer) { const { user, originalUrl: target } = req; if (user || isSharedDocAccess(target)) { try { - await onValidation(user, req, res); + await onValidation(user as any, req, res); } catch (e) { if (onError) { onError(req, res, e); -- cgit v1.2.3-70-g09d2 From 26e215b0cddbb4c14cfd8eb7a720a373e797c615 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Tue, 15 Oct 2019 00:23:00 -0400 Subject: cleaned up current_user_utils. cleaned up some interactions with colorBox, textbox height --- src/client/documents/Documents.ts | 19 +- src/client/views/DocComponent.tsx | 34 ++- src/client/views/InkingControl.tsx | 8 + src/client/views/MainView.tsx | 16 +- src/client/views/nodes/ColorBox.scss | 7 +- src/client/views/nodes/ColorBox.tsx | 12 +- src/client/views/nodes/FormattedTextBox.tsx | 13 +- src/client/views/nodes/PDFBox.tsx | 3 +- .../authentication/models/current_user_utils.ts | 295 ++++++++++----------- 9 files changed, 229 insertions(+), 178 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 1f89d2993..13cc7815c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -40,7 +40,7 @@ import { ButtonBox } from "../views/nodes/ButtonBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField"; import { PresBox } from "../views/nodes/PresBox"; -import { ComputedField } from "../../new_fields/ScriptField"; +import { ComputedField, ScriptField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; import { LinkFollowBox } from "../views/linking/LinkFollowBox"; @@ -65,7 +65,10 @@ export interface DocumentOptions { page?: number; scale?: number; fitWidth?: boolean; + forceActive?: boolean; + preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view layout?: string | Doc; + hideHeadings?: boolean; // whether stacking view column headings should be hidden isTemplate?: boolean; templates?: List; viewType?: number; @@ -84,11 +87,21 @@ export interface DocumentOptions { documentText?: string; borderRounding?: string; boxShadow?: string; + sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views schemaColumns?: List; dockingConfig?: string; autoHeight?: boolean; + removeDropProperties?: List; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document dbDoc?: Doc; + onClick?: ScriptField; + onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop icon?: string; + gridGap?: number; // gap between items in masonry view + xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts + yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts + panel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script + targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script + convertToButtons?: boolean; // whether documents dropped onto a collection should be converted to buttons that will construct the dropped document // [key: string]: Opt; } @@ -457,6 +470,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id); } + export function LinearDocument(documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id); + } + export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 2c5992259..b05966bb5 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,10 +1,17 @@ import * as React from 'react'; import { Doc } from '../../new_fields/Doc'; import { computed, action } from 'mobx'; -import { Cast } from '../../new_fields/Types'; +import { Cast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; +import { InkingControl } from './InkingControl'; +import { InkTool } from '../../new_fields/InkField'; -export function DocComponent

    (schemaCtor: (doc: Doc) => T) { + +/// DocComponents returns a generic base class for React views of document fields that are not interactive +interface DocComponentProps { + Document: Doc; +} +export function DocComponent

    (schemaCtor: (doc: Doc) => T) { class Component extends React.Component

    { //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed @@ -15,6 +22,27 @@ export function DocComponent

    (schemaCtor: (doc: D return Component; } + +/// DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox) +interface DocStaticProps { + Document: Doc; + isSelected: () => boolean; + renderDepth: number; +} +export function DocStaticComponent

    (schemaCtor: (doc: Doc) => T) { + class Component extends React.Component

    { + //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then + @computed + get Document(): T { + return schemaCtor(this.props.Document); + } + active = () => (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0) && !InkingControl.Instance.selectedTool; + } + return Component; +} + + +/// DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image) interface DocAnnotatableProps { Document: Doc; DataDoc?: Doc; @@ -57,7 +85,7 @@ export function DocAnnotatableComponent

    (schema return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc); } whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); - active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; + active = () => (InkingControl.Instance.selectedTool === InkTool.None) && (BoolCast(this.props.Document.forceActive) || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0); } return Component; } \ No newline at end of file diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 38734a03d..d42d8d2d9 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -48,6 +48,14 @@ export class InkingControl extends React.Component { let selected = SelectionManager.SelectedDocuments(); let oldColors = selected.map(view => { let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); + let sel = window.getSelection(); + if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) { + targetDoc.color = this._selectedColor; + return { + target: targetDoc, + previous: StrCast(targetDoc.color) + }; + } let oldColor = StrCast(targetDoc.backgroundColor); let matchedColor = this._selectedColor; const cvd = view.props.ContainingCollectionDoc; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ddea3e223..701f094e2 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -343,12 +343,12 @@ export class MainView extends React.Component { if (!(sidebarContent instanceof Doc)) { return (null); } - let libraryButtonDoc = Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc; - libraryButtonDoc.columnWidth = this.flyoutWidth / 3 - 30; + let sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; + sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30; return

    { - Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc); + Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); return true; } remButtonDoc = (doc: Doc) => { - Doc.RemoveDocFromList(CurrentUserUtils.UserDocument, "docButtons", doc); + Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); return true; } @computed get docButtons() { return
    - ; +const ColorDocument = makeInterface(documentSchema); @observer -export class ColorBox extends React.Component { +export class ColorBox extends DocStaticComponent(ColorDocument) { public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); } render() { - return
    + return
    e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
    ; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index b05d0046c..181f37d36 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -43,7 +43,6 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format import React = require("react"); import { ContextMenuProps } from '../ContextMenuItem'; import { ContextMenu } from '../ContextMenu'; -import { TextShadowProperty } from 'csstype'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -490,7 +489,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe ); this._heightReactionDisposer = reaction( - () => this.props.Document[WidthSym](), + () => [this.props.Document[WidthSym](), this.props.Document.autoHeight], () => this.tryUpdateHeight() ); @@ -984,13 +983,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action tryUpdateHeight() { - const ChromeHeight = this.props.ChromeHeight; - let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { + let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; + if (!this.props.Document.isAnimating && this.props.Document.autoHeight && scrollHeight !== 0 && + getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); - this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); - this.dataDoc.nativeHeight = nh ? sh : undefined; + this.props.Document.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); + this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; } } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 63b412a23..19797400f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -115,7 +115,6 @@ export class PDFBox extends DocAnnotatableComponent private newScriptChange = (e: React.ChangeEvent) => this._scriptValue = e.currentTarget.value; whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); - active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; searchStringChanged = (e: React.ChangeEvent) => this._searchString = e.currentTarget.value; @@ -205,7 +204,7 @@ export class PDFBox extends DocAnnotatableComponent _initialScale: number | undefined; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live.... render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive"); + let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf; if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale; if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 73cac879e..8af6bbfd5 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -1,20 +1,17 @@ -import { action, computed, observable, runInAction, reaction } from "mobx"; +import { action, computed, observable, reaction, runInAction } from "mobx"; import * as rp from 'request-promise'; import { DocServer } from "../../../client/DocServer"; import { Docs } from "../../../client/documents/Documents"; import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea"; import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; -import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView"; -import { CollectionView } from "../../../client/views/collections/CollectionView"; +import { UndoManager } from "../../../client/util/UndoManager"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { Cast, PromiseValue } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; import { RouteStore } from "../../RouteStore"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { ButtonBox } from "../../../client/views/nodes/ButtonBox"; -import { UndoManager } from "../../../client/util/UndoManager"; export class CurrentUserUtils { private static curr_id: string; @@ -22,176 +19,172 @@ export class CurrentUserUtils { private static mainDocId: string | undefined; public static get id() { return this.curr_id; } - @computed public static get UserDocument() { return Doc.UserDoc(); } public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } + @computed public static get UserDocument() { return Doc.UserDoc(); } @observable public static GuestTarget: Doc | undefined; @observable public static GuestWorkspace: Doc | undefined; private static createUserDocument(id: string): Doc { let doc = new Doc(id, true); - doc.viewType = CollectionViewType.Tree; - doc.layout = CollectionView.LayoutString(); doc.title = Doc.CurrentUserEmail; - doc.data = new List(); - doc.gridGap = 5; - doc.xMargin = 5; - doc.yMargin = 5; - doc.height = 42; - doc.boxShadow = "0 0"; - doc.convertToButtons = true; // for CollectionLinearView used as the docButton layout - doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" }); return this.updateUserDocument(doc);// this should be the last } - static updateUserDocument(doc: Doc) { + // a default set of note types .. not being used yet... + static setupNoteTypes(doc: Doc) { + let notes = [ + Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), + Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), + Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), + Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true }) + ]; + doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); + } - if (doc.undoBtn === undefined) { - doc.undoBtn = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "undo-alt" }); - (doc.undoBtn as Doc).onClick = ScriptField.MakeScript('undo()'); - Doc.AddDocToList(doc, "docButtons", doc.undoBtn as Doc); - } - if (doc.redoBtn === undefined) { - doc.redoBtn = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "redo-alt" }); - (doc.redoBtn as Doc).onClick = ScriptField.MakeScript('redo()'); - Doc.AddDocToList(doc, "docButtons", doc.redoBtn as Doc); - } + // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools + static setupCreatorButtons() { + let docProtoData = [ + { title: "collection", icon: "folder", script: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, + { title: "web page", icon: "globe-asia", script: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, + { title: "image", icon: "cat", script: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, + { title: "button", icon: "bolt", script: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, + { title: "presentation", icon: "tv", script: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })' }, + { title: "import folder", icon: "cloud-upload-alt", script: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, + ]; + return docProtoData.map(data => Docs.Create.FontIconDocument({ + nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, + title: data.title, icon: data.icon, onDragStart: ScriptField.MakeFunction(data.script) + })); + } + + // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer) + static setupCreatePanel(sidebarContainer: Doc) { + // setup a masonry view of all he creators + const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(), { + width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" + }); + // setup a color picker + const color = Docs.Create.ColorDocument({ + title: "color picker", width: 400, removeDropProperties: new List(["dropAction", "forceActive"]) + }); + color.dropAction = "alias"; // these must be set on the view document so they can't be part of the creator above. + color.forceActive = true; + + return Docs.Create.ButtonDocument({ + width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create", targetContainer: sidebarContainer, + panel: Docs.Create.StackingDocument([dragCreators, color], { + width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" + }), + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + }); + } + + // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views + static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) { // setup workspaces library item - if (doc.workspaces === undefined) { - const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 }); - workspaces.boxShadow = "0 0"; - doc.workspaces = workspaces; - } - PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => { - if (workspaces) { - workspaces.backgroundColor = "#eeeeee"; - workspaces.preventTreeViewOpen = true; - workspaces.forceActive = true; - workspaces.lockedPosition = true; - if (StrCast(workspaces.title) === "Workspaces") { - workspaces.title = "WORKSPACES"; - } - } + doc.workspaces = Docs.Create.TreeDocument([], { + title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee" }); - // setup notes list - if (doc.noteTypes === undefined) { - let notes = [Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), - Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), - Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), - Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true })]; - const noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); - doc.noteTypes = noteTypes; - } - PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast)); + doc.documents = Docs.Create.TreeDocument([], { + title: "DOCUMENTS", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true + }); // setup Recently Closed library item - if (doc.recentlyClosed === undefined) { - const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed".toUpperCase(), height: 75 }); - recentlyClosed.boxShadow = "0 0"; - doc.recentlyClosed = recentlyClosed; - } - PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => { - if (recent) { - recent.backgroundColor = "#eeeeee"; - recent.preventTreeViewOpen = true; - recent.forceActive = true; - recent.lockedPosition = true; - if (StrCast(recent.title) === "Recently Closed") { - recent.title = "RECENTLY CLOSED"; - } - } + doc.recentlyClosed = Docs.Create.TreeDocument([], { + title: "Recently Closed".toUpperCase(), height: 75, boxShadow: "0 0", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, backgroundColor: "#eeeeee" }); + return Docs.Create.ButtonDocument({ + width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library", + panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { + title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true + }), + targetContainer: sidebarContainer, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + }); + } - if (doc.curPresentation === undefined) { - const curPresentation = Docs.Create.PresDocument(new List(), { title: "Presentation" }); - curPresentation.boxShadow = "0 0"; - doc.curPresentation = curPresentation; - } + // setup the Search button which will display the search panel. + static setupSearchPanel(sidebarContainer: Doc) { + return Docs.Create.ButtonDocument({ + width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search", + panel: Docs.Create.QueryDocument({ + title: "search stack", ignoreClick: true + }), + targetContainer: sidebarContainer, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + }); + } - if (doc.Library === undefined) { - let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search" }); - let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library" }); - let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create" }); - if (doc.sidebarContainer === undefined) { - doc.sidebarContainer = new Doc(); - (doc.sidebarContainer as Doc).chromeStatus = "disabled"; - } + // setup the list of sidebar mode buttons which determine what is displayed in the sidebar + static setupSidebarButtons(doc: Doc) { + doc.sidebarContainer = new Doc(); + (doc.sidebarContainer as Doc).chromeStatus = "disabled"; - const library = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Library" }); - library.forceActive = true; - library.lockedPosition = true; - library.gridGap = 5; - library.xMargin = 5; - library.yMargin = 5; - library.dropAction = "alias"; - Library.targetContainer = doc.sidebarContainer; - Library.library = library; - Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library"); + doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc); + doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc); + doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc); - const searchBox = Docs.Create.QueryDocument({ title: "search stack" }); - searchBox.ignoreClick = true; - Search.searchBox = searchBox; - Search.targetContainer = doc.sidebarContainer; - Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox"); + // Finally, setup the list of buttons to display in the sidebar + doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.CreateBtn as Doc], { + width: 500, height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true, + backgroundColor: "lightgrey", chromeStatus: "disabled", title: "library stack" + }); + } - let createCollection = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" }); - createCollection.onDragStart = ScriptField.MakeFunction('Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })'); - let createWebPage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); - createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })'); - let createCatImage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); - createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })'); - let createButton = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); - createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })'); - let createPresentation = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); - createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })'); - let createFolderImport = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); - createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); - const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" }); - const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 }); - color.dropAction = "alias"; - color.ignoreClick = true; - color.removeDropProperties = new List(["dropAction", "ignoreClick"]); - const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" }); - Create.targetContainer = doc.sidebarContainer; - Create.creators = creators; - Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); + /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window + static setupExpandingButtons(doc: Doc) { + doc.undoBtn = Docs.Create.FontIconDocument( + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); + doc.redoBtn = Docs.Create.FontIconDocument( + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); - const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "library stack" }); - libraryButtons.sectionFilter = "title"; - libraryButtons.boxShadow = "0 0"; - libraryButtons.ignoreClick = true; - libraryButtons.hideHeadings = true; - libraryButtons.backgroundColor = "lightgrey"; + doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { + title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", + backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true + }); + } - doc.libraryButtons = libraryButtons; - doc.Library = Library; - doc.Create = Create; - doc.Search = Search; - } - PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { }); - PromiseValue(Cast(doc.Library, Doc)).then(library => library && library.library && library.targetContainer && (library.onClick as ScriptField).script.run({ this: library })); - PromiseValue(Cast(doc.Create, Doc)).then(async create => create && create.creators && create.targetContainer); - PromiseValue(Cast(doc.Search, Doc)).then(async search => search && search.searchBox && search.targetContainer); + // sets up the default set of documents to be shown in the Overlay layer + static setupOverlays(doc: Doc) { + doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" }); + doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" }); + Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc); + } - if (doc.overlays === undefined) { - const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" }); - Doc.GetProto(overlays).backgroundColor = "#aca3a6"; - doc.overlays = overlays; - } + // the initial presentation Doc to use + static setupDefaultPresentation(doc: Doc) { + doc.curPresentation = Docs.Create.PresDocument(new List(), { title: "Presentation", boxShadow: "0 0" }); + } - if (doc.linkFollowBox === undefined) { - PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" }))); - } + static setupMobileUploads(doc: Doc) { + doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" }); + } + + static updateUserDocument(doc: Doc) { + (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc); + (doc.noteTypes === undefined) && CurrentUserUtils.setupNoteTypes(doc); + (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc); + (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc); + (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc); + (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc); + + // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved. + PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast)); + PromiseValue(Cast(doc.sidebarButtons, Doc)).then(stackingDoc => { + stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => { + sidebarButtons && sidebarButtons.map((sidebarBtn, i) => { + sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => { + btn && btn.panel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn }); + }); + }); + }); + }); - doc.title = "DOCUMENTS"; - doc.backgroundColor = "#eeeeee"; - doc.width = 100; - doc.preventTreeViewOpen = true; - doc.forceActive = true; - doc.lockedPosition = true; + // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => (doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => (doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); @@ -215,12 +208,8 @@ export class CurrentUserUtils { await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => { if (id && id !== "guest") { return DocServer.GetRefField(id).then(async field => { - if (field instanceof Doc) { - await this.updateUserDocument(field); - runInAction(() => Doc.SetUserDoc(field)); - } else { - runInAction(() => Doc.SetUserDoc(this.createUserDocument(id))); - } + let userDoc = field instanceof Doc ? await this.updateUserDocument(field) : this.createUserDocument(id); + runInAction(() => Doc.SetUserDoc(userDoc)); }); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); -- cgit v1.2.3-70-g09d2 From 03f86e3b7b450699073c47aa43af23d31e0765d4 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 15 Oct 2019 13:42:13 -0400 Subject: extensions for linearViews or nesting. --- src/client/documents/Documents.ts | 2 +- src/client/views/CollectionLinearView.scss | 24 +++--- src/client/views/CollectionLinearView.tsx | 88 +++++++++++----------- src/client/views/DocComponent.tsx | 2 +- src/client/views/InkingControl.tsx | 58 +++++--------- src/client/views/MainView.tsx | 80 ++++++++++++-------- src/client/views/collections/CollectionSubView.tsx | 1 - src/client/views/nodes/DocumentView.tsx | 15 ++-- src/client/views/nodes/FontIconBox.tsx | 5 +- .../authentication/models/current_user_utils.ts | 26 ++++--- 10 files changed, 154 insertions(+), 147 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5794a6bee..6df172f46 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -471,7 +471,7 @@ export namespace Docs { } export function LinearDocument(documents: Array, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id); } export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array, options: DocumentOptions) { diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss index 4bfd88b69..4423a7020 100644 --- a/src/client/views/CollectionLinearView.scss +++ b/src/client/views/CollectionLinearView.scss @@ -4,26 +4,25 @@ .collectionLinearView-outer{ overflow: hidden; height:100%; - padding:5px; .collectionLinearView { display:flex; + height: 100%; >label { background: $dark-color; color: $light-color; display: inline-block; border-radius: 18px; - font-size: 25px; - width: 36px; - height: 36px; - margin-right: 10px; + font-size: 12.5px; + width: 18px; + height: 18px; + margin-top:auto; + margin-bottom:auto; cursor: pointer; transition: transform 0.2s; } label p { - padding-left: 10.5px; - width: 500px; - height: 500px; + padding-left:5px; } label:hover { @@ -47,20 +46,21 @@ .collectionLinearView-content { display: flex; opacity: 1; - padding: 0; position: relative; + margin-top: auto; .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { position:relative; - margin-right: 10px; + margin-top: auto; + margin-bottom: auto; } .collectionLinearView-docBtn-scalable:hover { transform: scale(1.15); } .collectionLinearView-round-button { - width: 36px; - height: 36px; + width: 18px; + height: 18px; border-radius: 18px; font-size: 15px; } diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 5c793f784..9d1dd7749 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -1,23 +1,20 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, computed, IReactionDisposer, reaction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc'; -import { InkTool } from '../../new_fields/InkField'; +import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc'; import { ObjectField } from '../../new_fields/ObjectField'; +import { makeInterface } from '../../new_fields/Schema'; import { ScriptField } from '../../new_fields/ScriptField'; -import { NumCast, StrCast } from '../../new_fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, returnFalse, Utils } from '../../Utils'; +import { BoolCast, NumCast, StrCast } from '../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; -import { UndoManager } from '../util/UndoManager'; -import { InkingControl } from './InkingControl'; -import { DocumentView, documentSchema } from './nodes/DocumentView'; import "./CollectionLinearView.scss"; -import { makeInterface } from '../../new_fields/Schema'; +import { CollectionViewType } from './collections/CollectionBaseView'; import { CollectionSubView } from './collections/CollectionSubView'; -import { DocumentType } from '../documents/DocumentTypes'; +import { documentSchema, DocumentView } from './nodes/DocumentView'; +import { translate } from 'googleapis/build/src/apis/translate'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -26,7 +23,6 @@ const LinearDocument = makeInterface(documentSchema); @observer export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef(); - @observable private _checked = false; private _dropDisposer?: DragManager.DragDropDisposer; private _heightDisposer?: IReactionDisposer; @@ -37,12 +33,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). - this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this._checked ? 1 : 0), - () => { - if (true || this.props.Document.fitWidth) { - this.props.Document.width = 36 + (this._checked ? this.childDocs.length * (this.props.Document[HeightSym]() + 10) : 10); - } - }, + this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), + () => this.props.Document.width = 18 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), { fireImmediately: true } ); } @@ -56,11 +48,13 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { drop = action((e: Event, de: DragManager.DropEvent) => { (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { let dbox = doc; - if (!doc.onDragStart && this.props.Document.convertToButtons) { + if (!doc.onDragStart && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); dbox.dragFactory = doc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); + } else if (doc.viewType === CollectionViewType.Linear) { + dbox.ignoreClick = true; } (de.data as DragManager.DocumentDragData).droppedDocuments[i] = dbox; }); @@ -68,40 +62,51 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { return super.drop(e, de); }); - selected = (tool: InkTool) => { - if (!InkingControl.Instance || InkingControl.Instance.selectedTool === InkTool.None) return { display: "none" }; - if (InkingControl.Instance.selectedTool === tool) { - return { color: "#61aaa3", fontSize: "50%" }; - } - return { fontSize: "50%" }; - } - public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } - dimension = () => NumCast(this.props.Document.height) - 5; + dimension = () => NumCast(this.props.Document.height); // 2 * the padding + getTransform = (ele: React.RefObject) => () => { + if (!ele.current) return Transform.Identity(); + let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); + return new Transform(-translateX, -translateY, 1 / scale); + }; + _spacing = 20; render() { let guid = Utils.GenerateGuid(); return
    - this._checked = this.addMenuToggle.current!.checked)} /> - + this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> +
    - {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => -
    + {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => { + let nested = pair.layout.viewType === CollectionViewType.Linear; + let dref = React.createRef(); + let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension()); + let scalingContent = nested ? 1 : this.dimension() / (this._spacing + nativeWidth); + let scalingBox = nested ? 1 : this.dimension() / nativeWidth; + let deltaSize = nativeWidth * scalingBox - nativeWidth * scalingContent; + return
    this.dimension() / (10 + NumCast(pair.layout.nativeWidth, this.dimension()))} // ugh - need to get rid of this inline function to avoid recomputing - PanelWidth={this.dimension} - PanelHeight={this.dimension} + ScreenToLocalTransform={this.getTransform(dref)} + ContentScaling={() => scalingContent} // ugh - need to get rid of this inline function to avoid recomputing + PanelWidth={() => nested ? pair.layout[WidthSym]() : this.dimension()} + PanelHeight={() => nested ? pair.layout[HeightSym]() : this.dimension()} renderDepth={this.props.renderDepth + 1} focus={emptyFunction} backgroundColor={returnEmptyString} @@ -113,15 +118,10 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { zoomToScale={emptyFunction} getScale={returnOne}> -
    )} +
    + })} {/*
  • */} - {this.props.showHiddenControls ? <> - - - - - - : (null)} +
    ; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index b05966bb5..fbc27192c 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -36,7 +36,7 @@ export function DocStaticComponent

    (schemaCtor: (doc get Document(): T { return schemaCtor(this.props.Document); } - active = () => (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0) && !InkingControl.Instance.selectedTool; + active = () => (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools } return Component; } diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index d42d8d2d9..41dec9f83 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,38 +1,30 @@ -import { observable, action, computed, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { ColorResult } from 'react-color'; -import React = require("react"); -import { observer } from "mobx-react"; -import "./InkingControl.scss"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons'; -import { SelectionManager } from "../util/SelectionManager"; -import { InkTool } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; -import { undoBatch, UndoManager } from "../util/UndoManager"; -import { StrCast, NumCast, Cast } from "../../new_fields/Types"; -import { listSpec } from "../../new_fields/Schema"; +import { InkTool } from "../../new_fields/InkField"; import { List } from "../../new_fields/List"; +import { listSpec } from "../../new_fields/Schema"; +import { Cast, NumCast, StrCast } from "../../new_fields/Types"; import { Utils } from "../../Utils"; +import { Scripting } from "../util/Scripting"; +import { SelectionManager } from "../util/SelectionManager"; +import { undoBatch, UndoManager } from "../util/UndoManager"; -library.add(faPen, faHighlighter, faEraser, faBan); -@observer -export class InkingControl extends React.Component { +export class InkingControl { @observable static Instance: InkingControl; @observable private _selectedTool: InkTool = InkTool.None; @observable private _selectedColor: string = "rgb(244, 67, 54)"; @observable private _selectedWidth: string = "5"; @observable public _open: boolean = false; - constructor(props: Readonly<{}>) { - super(props); - runInAction(() => InkingControl.Instance = this); + constructor() { + InkingControl.Instance = this; } - @action - switchTool = (tool: InkTool): void => { + switchTool = action((tool: InkTool): void => { this._selectedTool = tool; - } + }) decimalToHexString(number: number) { if (number < 0) { number = 0xFFFFFFFF + number + 1; @@ -124,22 +116,10 @@ export class InkingControl extends React.Component { return this._selectedWidth; } - @action - toggleDisplay = () => { - this._open = !this._open; - this.switchTool(this._open ? InkTool.Pen : InkTool.None); - } - render() { - return ( -

      -
    • - - ) => this.switchWidth(e.target.value)} /> - ) => this.switchWidth(e.target.value)} /> -
    • -
    - ); - } -} \ No newline at end of file +} +Scripting.addGlobal(function activatePen() { return InkingControl.Instance.switchTool(InkTool.Pen); }); +Scripting.addGlobal(function activateBrush() { return InkingControl.Instance.switchTool(InkTool.Highlighter); }); +Scripting.addGlobal(function activateEraser() { return InkingControl.Instance.switchTool(InkTool.Eraser); }); +Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); }); +Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); }); +Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); }); \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 701f094e2..4e06763a4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,8 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { + faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, + faMusic, faObjectGroup, faPause, faPenNib, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -99,6 +102,8 @@ export class MainView extends React.Component { library.add(faGlobeAsia); library.add(faUndoAlt); library.add(faRedoAlt); + library.add(faPen); + library.add(faEraser); library.add(faPenNib); library.add(faFilm); library.add(faMusic); @@ -450,37 +455,50 @@ export class MainView extends React.Component { Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); return true; } + @action + moveButtonDoc = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { + return this.remButtonDoc(doc) && addDocument(doc); + } + buttonBarXf = () => { + if (!this._docBtnRef.current) return Transform.Identity(); + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); + return new Transform(-translateX, -translateY, 1 / scale); + } + _docBtnRef = React.createRef(); @computed get docButtons() { - return
    - - -
    ; + console.log("stuff = " + this.flyoutWidthFunc() + " " + this.getContentsHeight()); + if (CurrentUserUtils.UserDocument.expandingButtons instanceof Doc) { + return
    + + +
    ; + } + return (null); } render() { diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 6e8e4fa12..fdbe5339d 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -33,7 +33,6 @@ export interface CollectionViewProps extends FieldViewProps { VisibleHeight?: () => number; chromeCollapsed: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; - showHiddenControls?: boolean; // hack for showing the undo/redo/ink controls in a linear view -- needs to be redone } export interface SubCollectionViewProps extends CollectionViewProps { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 62264ea38..c0e5185c1 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -39,6 +39,7 @@ import { ImageField } from '../../../new_fields/URLField'; import SharingManager from '../../util/SharingManager'; import { Scripting } from '../../util/Scripting'; import { DictationOverlay } from '../DictationOverlay'; +import { CollectionViewType } from '../collections/CollectionBaseView'; library.add(fa.faEdit); library.add(fa.faTrash); @@ -105,7 +106,7 @@ export const documentSchema = createSchema({ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy") removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) - onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return an Doc to drag. + onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. dragFactory: Doc, // the document that serves as the "template" for the onDragStart script ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents @@ -167,7 +168,7 @@ export class DocumentView extends DocComponent(Docu if (this._mainCont.current) { let dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - dragData.offset = this.Document.onDragStart ? [0, 0] : this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; @@ -175,7 +176,7 @@ export class DocumentView extends DocComponent(Docu handlers: { dragComplete: action((emptyFunction)) }, - hideSource: !dropAction && !this.Document.onDragStart + hideSource: !dropAction && !this.Document.onDragStart && !this.Document.onClick }); } } @@ -249,7 +250,7 @@ export class DocumentView extends DocComponent(Docu this._hitTemplateDrag = true; } } - if ((this.active || this.Document.onDragStart) && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + if ((this.active || this.Document.onDragStart || this.Document.onClick) && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -261,9 +262,9 @@ export class DocumentView extends DocComponent(Docu if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.Document.onDragStart) && e.buttons === 1) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); @@ -684,7 +685,7 @@ export class DocumentView extends DocComponent(Docu color: StrCast(this.Document.color), outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - background: backgroundColor, + background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 3f5afb6d1..aa442cd92 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -5,6 +5,7 @@ import { createSchema, makeInterface } from '../../../new_fields/Schema'; import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { StrCast } from '../../../new_fields/Types'; const FontIconSchema = createSchema({ icon: "string" }); @@ -16,6 +17,8 @@ export class FontIconBox extends DocComponent( public static LayoutString() { return FieldView.LayoutString(FontIconBox); } render() { - return ; + return ; } } \ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 8af6bbfd5..c4b91dadd 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -12,6 +12,7 @@ import { ScriptField } from "../../../new_fields/ScriptField"; import { Cast, PromiseValue } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; import { RouteStore } from "../../RouteStore"; +import { InkingControl } from "../../../client/views/InkingControl"; export class CurrentUserUtils { private static curr_id: string; @@ -45,17 +46,21 @@ export class CurrentUserUtils { // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools static setupCreatorButtons() { - let docProtoData = [ - { title: "collection", icon: "folder", script: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, - { title: "web page", icon: "globe-asia", script: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, - { title: "image", icon: "cat", script: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, - { title: "button", icon: "bolt", script: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, - { title: "presentation", icon: "tv", script: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })' }, - { title: "import folder", icon: "cloud-upload-alt", script: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, + let docProtoData: { title: string, icon: string, drag?: string, click?: string }[] = [ + { title: "collection", icon: "folder", drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, + { title: "web page", icon: "globe-asia", drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, + { title: "image", icon: "cat", drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, + { title: "button", icon: "bolt", drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, + { title: "presentation", icon: "tv", drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })' }, + { title: "import folder", icon: "cloud-upload-alt", drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, + { title: "pen", icon: "pen-nib", click: 'activatePen(); setInkWidth(2);' }, + { title: "highlighter", icon: "pen", click: 'activateBrush(); setInkWidth(20);' }, + { title: "eraser", icon: "eraser", click: 'activateEraser();' }, + { title: "none", icon: "pause", click: 'deactivateInk();' }, ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ - nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, - title: data.title, icon: data.icon, onDragStart: ScriptField.MakeFunction(data.script) + nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "alias" : undefined, + title: data.title, icon: data.icon, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined })); } @@ -144,7 +149,7 @@ export class CurrentUserUtils { doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", - backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true + backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true }); } @@ -165,6 +170,7 @@ export class CurrentUserUtils { } static updateUserDocument(doc: Doc) { + new InkingControl(); (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc); (doc.noteTypes === undefined) && CurrentUserUtils.setupNoteTypes(doc); (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc); -- cgit v1.2.3-70-g09d2 From 33811c112c7e479813908ba10f72813954a3e289 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 15 Oct 2019 16:25:10 -0400 Subject: working version of inking buttons --- src/client/documents/Documents.ts | 2 ++ src/client/util/DragManager.ts | 4 +-- src/client/util/SelectionManager.ts | 11 -------- src/client/views/CollectionLinearView.tsx | 4 +-- src/client/views/InkingControl.tsx | 6 ++-- src/client/views/MainView.tsx | 6 ++-- src/client/views/nodes/ColorBox.tsx | 33 ++++++++++++++++++++-- src/client/views/nodes/DocumentView.tsx | 7 +++-- src/client/views/nodes/FontIconBox.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 16 ++++++----- src/new_fields/Doc.ts | 4 +++ .../authentication/models/current_user_utils.ts | 31 +++++++++++--------- 12 files changed, 78 insertions(+), 48 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6df172f46..f4fce34ac 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -93,6 +93,8 @@ export interface DocumentOptions { autoHeight?: boolean; removeDropProperties?: List; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document dbDoc?: Doc; + unchecked?: ScriptField; // returns whether a check box is unchecked + activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) onClick?: ScriptField; onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop icon?: string; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 169f2edec..92666c03c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -234,9 +234,9 @@ export namespace DragManager { export let StartDragFunctions: (() => void)[] = []; - export async function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { runInAction(() => StartDragFunctions.map(func => func())); - await dragData.draggedDocuments.map(d => d.dragFactory); + dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag : (dropData: { [id: string]: any }) => { (dropData.droppedDocuments = diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index df1b46b33..2d717ca57 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -45,17 +45,6 @@ export namespace SelectionManager { } const manager = new Manager(); - reaction(() => manager.SelectedDocuments, sel => { - let targetColor = "#FFFFFF"; - if (sel.length > 0) { - let firstView = sel[0]; - let doc = firstView.props.Document; - let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); - let stored = StrCast(targetDoc.backgroundColor); - stored.length > 0 && (targetColor = stored); - } - InkingControl.Instance.updateSelectedColor(targetColor); - }, { fireImmediately: true }); export function DeselectDoc(docView: DocumentView): void { manager.DeselectDoc(docView); diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 9d1dd7749..eb3c768d0 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -48,7 +48,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { drop = action((e: Event, de: DragManager.DropEvent) => { (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { let dbox = doc; - if (!doc.onDragStart && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { + if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" }); dbox.dragFactory = doc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; @@ -90,7 +90,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { return
    (); @computed get docButtons() { - console.log("stuff = " + this.flyoutWidthFunc() + " " + this.getContentsHeight()); if (CurrentUserUtils.UserDocument.expandingButtons instanceof Doc) { - return
    + return
    ; const ColorDocument = makeInterface(documentSchema); @@ -14,9 +17,35 @@ const ColorDocument = makeInterface(documentSchema); @observer export class ColorBox extends DocStaticComponent(ColorDocument) { public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); } + _selectedDisposer: IReactionDisposer | undefined; + componentDidMount() { + this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(), + action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"), + { fireImmediately: true }); + + // compare to this reaction that used to be in Selection Manager + // reaction(() => manager.SelectedDocuments, sel => { + // let targetColor = "#FFFFFF"; + // if (sel.length > 0) { + // let firstView = sel[0]; + // let doc = firstView.props.Document; + // let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); + // let stored = StrCast(targetDoc.backgroundColor); + // stored.length > 0 && (targetColor = stored); + // } + // InkingControl.Instance.updateSelectedColor(targetColor); + // }, { fireImmediately: true }); + } + componentWillUnmount() { + this._selectedDisposer && this._selectedDisposer(); + } + + @observable _startupColor = "black"; + render() { - return
    e.button === 0 && !e.ctrlKey && e.stopPropagation()}> - + return
    e.button === 0 && !e.ctrlKey && e.stopPropagation()}> +
    ; } } \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c0e5185c1..6f99c541f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -107,7 +107,7 @@ export const documentSchema = createSchema({ removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. - dragFactory: Doc, // the document that serves as the "template" for the onDragStart script + dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed @@ -676,6 +676,7 @@ export class DocumentView extends DocComponent(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; + let highlighting = fullDegree && this.props.Document.type !== DocumentType.FONTICON && this.props.Document.viewType !== CollectionViewType.Linear return (
    (Docu transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", color: StrCast(this.Document.color), - outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", - border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, + outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", + border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index aa442cd92..efe47d8a8 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -18,7 +18,7 @@ export class FontIconBox extends DocComponent( render() { return ; } } \ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 181f37d36..1bdff3ec7 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -927,16 +927,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onMouseUp = (e: React.MouseEvent): void => { e.stopPropagation(); - // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there are nested prosemirrors. We only want the lowest level prosemirror to be invoked. - if ((this._editorView as any).mouseDown) { - let originalUpHandler = (this._editorView as any).mouseDown.up; - (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler); - (this._editorView as any).mouseDown.up = (e: MouseEvent) => { + let view = this._editorView as any; + // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there + // are nested prosemirrors. We only want the lowest level prosemirror to be invoked. + if (view.mouseDown) { + let originalUpHandler = view.mouseDown.up; + view.root.removeEventListener("mouseup", originalUpHandler); + view.mouseDown.up = (e: MouseEvent) => { !(e as any).formattedHandled && originalUpHandler(e); - e.stopPropagation(); + // e.stopPropagation(); (e as any).formattedHandled = true; }; - (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up); + view.root.addEventListener("mouseup", view.mouseDown.up); } } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 4bed113e3..276596fb8 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -525,6 +525,7 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc { const copy = new Doc(copyProtoId, true); Object.keys(doc).forEach(key => { + let cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); if (key === "proto" && copyProto) { if (doc[key] instanceof Doc) { @@ -533,6 +534,8 @@ export namespace Doc { } else { if (field instanceof RefField) { copy[key] = field; + } else if (cfield instanceof ComputedField) { + copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); } else if (field instanceof ObjectField) { copy[key] = ObjectField.MakeCopy(field); } else if (field instanceof Promise) { @@ -735,5 +738,6 @@ Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return Doc.Make Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); Scripting.addGlobal(function aliasDocs(field: any) { return new List(field.map((d: any) => Doc.MakeAlias(d))); }); Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); +Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2) }); Scripting.addGlobal(function undo() { return UndoManager.Undo(); }); Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); \ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index c4b91dadd..3c4a46ed8 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -8,7 +8,7 @@ import { UndoManager } from "../../../client/util/UndoManager"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { ScriptField } from "../../../new_fields/ScriptField"; +import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; import { Cast, PromiseValue } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; import { RouteStore } from "../../RouteStore"; @@ -45,29 +45,32 @@ export class CurrentUserUtils { } // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools - static setupCreatorButtons() { - let docProtoData: { title: string, icon: string, drag?: string, click?: string }[] = [ + static setupCreatorButtons(doc: Doc) { + doc.activePen = doc; + let docProtoData: { title: string, icon: string, drag?: string, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string }[] = [ { title: "collection", icon: "folder", drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, { title: "web page", icon: "globe-asia", drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, { title: "image", icon: "cat", drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, { title: "button", icon: "bolt", drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, { title: "presentation", icon: "tv", drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })' }, { title: "import folder", icon: "cloud-upload-alt", drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, - { title: "pen", icon: "pen-nib", click: 'activatePen(); setInkWidth(2);' }, - { title: "highlighter", icon: "pen", click: 'activateBrush(); setInkWidth(20);' }, - { title: "eraser", icon: "eraser", click: 'activateEraser();' }, - { title: "none", icon: "pause", click: 'deactivateInk();' }, + { title: "pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "highlighter", icon: "pen", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "none", icon: "pause", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ - nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "alias" : undefined, - title: data.title, icon: data.icon, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined + nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, + onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, + unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen, + backgroundColor: data.backgroundColor })); } // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer) - static setupCreatePanel(sidebarContainer: Doc) { + static setupCreatePanel(sidebarContainer: Doc, doc: Doc) { // setup a masonry view of all he creators - const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(), { + const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" }); // setup a color picker @@ -129,7 +132,7 @@ export class CurrentUserUtils { doc.sidebarContainer = new Doc(); (doc.sidebarContainer as Doc).chromeStatus = "disabled"; - doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc); + doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc, doc); doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc); doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc); @@ -143,9 +146,9 @@ export class CurrentUserUtils { /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupExpandingButtons(doc: Doc) { doc.undoBtn = Docs.Create.FontIconDocument( - { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); doc.redoBtn = Docs.Create.FontIconDocument( - { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", -- cgit v1.2.3-70-g09d2 From fc0473d0322c9d00ab339c5058dac5863a63ae41 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Tue, 15 Oct 2019 21:36:09 -0400 Subject: fixes for dropAction and buttons --- src/client/documents/Documents.ts | 2 +- src/server/authentication/models/current_user_utils.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f4fce34ac..a5a9087c2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -306,7 +306,7 @@ export namespace Docs { */ export namespace Create { - const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; + const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "dropAction", "forceActive"]; /** * This function receives the relevant document prototype and uses diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 3c4a46ed8..0c9f09112 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -63,7 +63,7 @@ export class CurrentUserUtils { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen, - backgroundColor: data.backgroundColor + backgroundColor: data.backgroundColor, removeDropProperties: new List(["dropAction"]) })); } @@ -75,10 +75,8 @@ export class CurrentUserUtils { }); // setup a color picker const color = Docs.Create.ColorDocument({ - title: "color picker", width: 400, removeDropProperties: new List(["dropAction", "forceActive"]) + title: "color picker", width: 400, dropAction: "alias", forceActive: true, removeDropProperties: new List(["dropAction", "forceActive"]) }); - color.dropAction = "alias"; // these must be set on the view document so they can't be part of the creator above. - color.forceActive = true; return Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create", targetContainer: sidebarContainer, @@ -152,7 +150,7 @@ export class CurrentUserUtils { doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", - backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true + backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true, }); } -- cgit v1.2.3-70-g09d2 From 50fb344cd7c93539f00b3aebc8fa5be4aefc73fd Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 16 Oct 2019 11:07:36 -0400 Subject: small fixes: cursors, pdf, dragging buttons --- src/client/views/nodes/ColorBox.scss | 9 +++++++++ src/client/views/nodes/DocumentView.scss | 1 + src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FormattedTextBox.scss | 1 + src/client/views/nodes/KeyValueBox.scss | 1 + src/client/views/nodes/PDFBox.scss | 2 +- src/client/views/nodes/PDFBox.tsx | 6 +++--- src/server/authentication/models/current_user_utils.ts | 2 +- 8 files changed, 18 insertions(+), 6 deletions(-) (limited to 'src/server') diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss index 32a7891c6..bf334c939 100644 --- a/src/client/views/nodes/ColorBox.scss +++ b/src/client/views/nodes/ColorBox.scss @@ -6,6 +6,15 @@ .sketch-picker { margin:auto; + div { + cursor: crosshair; + } + .flexbox-fix { + cursor: pointer; + div { + cursor:pointer; + } + } } } .colorBox-container-interactive { diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b3e7898c1..69eb1a843 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -6,6 +6,7 @@ left:0; border-radius: inherit; transition : outline .3s linear; + cursor: grab; // background: $light-color; //overflow: hidden; transform-origin: left top; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8a320d39b..ebd0ca549 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -176,7 +176,7 @@ export class DocumentView extends DocComponent(Docu handlers: { dragComplete: action((emptyFunction)) }, - hideSource: !dropAction && !this.Document.onDragStart && !this.Document.onClick + hideSource: !dropAction && !this.Document.onDragStart }); } } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 29e8b14a8..0550f9708 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -12,6 +12,7 @@ .formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden { + cursor: text; background: inherit; padding: 0; border-width: 0px; diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 87a9565e8..6e8a36c6a 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -9,6 +9,7 @@ box-sizing: border-box; display: inline-block; pointer-events: all; + cursor: default; .imageBox-cont img { width: auto; } diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 1c1d6ec95..dd909e09f 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -15,7 +15,7 @@ width: 100%; height: 100%; background: lightslategray; - .pdfBox-title-cont, .pdfBox-cont-interactive{ + .pdfBox-cont, .pdfBox-cont-interactive{ width: 150%; height: 100%; position: relative; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 19797400f..f31128356 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -16,7 +16,6 @@ import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeF import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { DocAnnotatableComponent } from "../DocComponent"; -import { InkingControl } from "../InkingControl"; import { PDFViewer } from "../pdf/PDFViewer"; import { documentSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; @@ -65,6 +64,7 @@ export class PDFBox extends DocAnnotatableComponent } }, { fireImmediately: true }); } + loaded = (nw: number, nh: number, np: number) => { this.dataDoc.numPages = np; this.Document.nativeWidth = nw * 96 / 72; @@ -133,12 +133,12 @@ export class PDFBox extends DocAnnotatableComponent ; - return !this.props.active() ? (null) : + return !this.active() ? (null) : (
    e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}>
    e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> ; diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index dd909e09f..deb98dc8d 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -7,6 +7,7 @@ overflow: hidden; position:absolute; z-index: -1; + cursor:auto; } .pdfBox-title-outer { diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index b06d19c53..44e075153 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -15,7 +15,7 @@ } .pdfMenu-button:hover { - background-color: #121212; + background-color: #d4d4d4; } .pdfMenu-dragger { diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 4abf85381..0fe8d996f 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -47,20 +47,20 @@ export class CurrentUserUtils { // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools static setupCreatorButtons(doc: Doc) { doc.activePen = doc; - let docProtoData: { title: string, icon: string, drag?: string, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string }[] = [ - { title: "collection", icon: "folder", drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, - { title: "web page", icon: "globe-asia", drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, - { title: "image", icon: "cat", drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, - { title: "button", icon: "bolt", drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, - { title: "presentation", icon: "tv", drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })' }, - { title: "import folder", icon: "cloud-upload-alt", drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, - { title: "pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "highlighter", icon: "pen", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, - { title: "none", icon: "pause", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string }[] = [ + { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, + { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, + { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, + { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, + { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List(), { width: 200, height: 500, title: "a presentation trail" })' }, + { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, + { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, + { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this) && this.activePen.pen !== undefined`, backgroundColor: "white", activePen: doc }, ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ - nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, + nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen, backgroundColor: data.backgroundColor, removeDropProperties: new List(["dropAction"]) @@ -144,9 +144,9 @@ export class CurrentUserUtils { /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupExpandingButtons(doc: Doc) { doc.undoBtn = Docs.Create.FontIconDocument( - { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List(["dropAction"]), title: "undo button", icon: "undo-alt" }); doc.redoBtn = Docs.Create.FontIconDocument( - { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); + { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List(["dropAction"]), title: "redo button", icon: "redo-alt" }); doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", @@ -192,8 +192,8 @@ export class CurrentUserUtils { }); // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet - doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => (doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); - doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => (doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); + doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); return doc; } -- cgit v1.2.3-70-g09d2 From 789b4925dff316ed9151bf9d327ab6223f09ec4a Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 16 Oct 2019 17:07:42 -0400 Subject: added a default note template --- src/client/documents/Documents.ts | 1 + src/server/authentication/models/current_user_utils.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a5a9087c2..ccb08f4cd 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -96,6 +96,7 @@ export interface DocumentOptions { unchecked?: ScriptField; // returns whether a check box is unchecked activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) onClick?: ScriptField; + dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop icon?: string; gridGap?: number; // gap between items in masonry view diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 0fe8d996f..ef977c89a 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -35,20 +35,23 @@ export class CurrentUserUtils { // a default set of note types .. not being used yet... static setupNoteTypes(doc: Doc) { - let notes = [ + return [ Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), - Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true }) + Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true }), + Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplate: true }) ]; - doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); } // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools static setupCreatorButtons(doc: Doc) { + let notes = CurrentUserUtils.setupNoteTypes(doc); + doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); doc.activePen = doc; - let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string }[] = [ + let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, + { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] }, { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, @@ -63,7 +66,7 @@ export class CurrentUserUtils { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen, - backgroundColor: data.backgroundColor, removeDropProperties: new List(["dropAction"]) + backgroundColor: data.backgroundColor, removeDropProperties: new List(["dropAction"]), dragFactory: data.dragFactory })); } @@ -173,7 +176,6 @@ export class CurrentUserUtils { static updateUserDocument(doc: Doc) { new InkingControl(); (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc); - (doc.noteTypes === undefined) && CurrentUserUtils.setupNoteTypes(doc); (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc); (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc); (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc); -- cgit v1.2.3-70-g09d2 From 7763a08eb5ed931dbf854e2b72d07b7613791e2b Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 16 Oct 2019 21:27:25 -0400 Subject: some fixes to linearView for handling templates. fixed DOCUMENTS on library view. renaming a bunch of template stuff... still in progress. --- src/client/documents/Documents.ts | 1 + src/client/views/CollectionLinearView.tsx | 18 ++++++++++++++---- src/client/views/DocComponent.tsx | 2 +- src/client/views/DocumentDecorations.tsx | 4 ++-- src/client/views/InkingControl.tsx | 7 ++----- src/client/views/TemplateMenu.tsx | 2 +- src/client/views/collections/CollectionBaseView.tsx | 10 +++++----- .../views/collections/CollectionStackingView.tsx | 4 +++- src/client/views/collections/CollectionSubView.tsx | 11 ++++++++--- src/client/views/collections/CollectionTreeView.tsx | 3 ++- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/ButtonBox.tsx | 6 +++++- src/client/views/nodes/DocumentView.tsx | 6 +++--- src/client/views/nodes/FormattedTextBox.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 2 +- src/new_fields/Doc.ts | 6 +++--- src/server/authentication/models/current_user_utils.ts | 16 ++++++++-------- 17 files changed, 61 insertions(+), 41 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ccb08f4cd..2ffbc8394 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -70,6 +70,7 @@ export interface DocumentOptions { layout?: string | Doc; hideHeadings?: boolean; // whether stacking view column headings should be hidden isTemplate?: boolean; + isTemplateDoc?: boolean; templates?: List; viewType?: number; backgroundColor?: string; diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 44d9b042e..3e2ab1459 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -1,7 +1,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc'; +import { Doc, HeightSym, WidthSym, DocListCast } from '../../new_fields/Doc'; import { ObjectField } from '../../new_fields/ObjectField'; import { makeInterface } from '../../new_fields/Schema'; import { ScriptField } from '../../new_fields/ScriptField'; @@ -51,9 +51,19 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { let dbox = doc; if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { - let template = doc.layout instanceof Doc && doc.layout.isTemplate ? doc.layout : doc; - template.isTemplate = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction; - dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplate ? "font" : "bolt" }); + let template = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; + if (template.type === DocumentType.COL) { + let layout = StrCast(template.layout).match(/fieldKey={"[^"]*"}/)![0]; + let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, ""); + let docs = DocListCast(template[fieldKey]); + docs.map(d => { + Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(template)); + }); + template.isTemplateDoc = true; + } else { + template.isTemplateDoc = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction; + } + dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplateDoc ? "font" : "bolt" }); dbox.dragFactory = template; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index fbc27192c..b6b717be0 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -68,7 +68,7 @@ export function DocAnnotatableComponent

    (schema return index !== -1 && value.splice(index, 1) ? true : false; } - @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; } + @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; } @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2f40ea746..6e8ba2d3d 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -380,7 +380,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`); usingRule = usingRule || (ruleProvider && heading ? true : false); }); - !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)). + !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateField ? dv.props.Document : Doc.GetProto(dv.props.Document)). map(d => d.borderRounding = `${Math.min(100, dist)}%`); e.stopPropagation(); e.preventDefault(); @@ -481,7 +481,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let actualdH = Math.max(height + (dH * scale), 20); layoutDoc.x = (layoutDoc.x || 0) + dX * (actualdW - width); layoutDoc.y = (layoutDoc.y || 0) + dY * (actualdH - height); - let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here... + let proto = doc.isTemplateField ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here... let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight); if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) { layoutDoc.ignoreAspect = false; diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 46c6fae1c..bc5249acd 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -43,7 +43,7 @@ export class InkingControl { let oldColors = selected.map(view => { let targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : view.props.Document.layout instanceof Doc ? view.props.Document.layout : - view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); + view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document); let sel = window.getSelection(); if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) { targetDoc.color = this._selectedColor; @@ -81,10 +81,7 @@ export class InkingControl { ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined; ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb))); } - if (!ruleProvider) { - if (targetDoc) - targetDoc.backgroundColor = matchedColor; - } + (!ruleProvider && targetDoc) && (targetDoc.backgroundColor = matchedColor); return { target: targetDoc, diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 9e5e62e03..da776f887 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -100,7 +100,7 @@ export class TemplateMenu extends React.Component { clearTemplates = (event: React.MouseEvent) => { Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined)); ["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => { - if (d.Document.isTemplate && d.props.DataDoc) { + if (d.Document.isTemplateDoc && d.props.DataDoc) { d.Document[field] = undefined; } else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) { d.Document[field] = Doc.GetProto(d.Document)[field] = undefined; diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 7798964ea..15853fcae 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -84,7 +84,7 @@ export class CollectionBaseView extends React.Component { } } - @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; } active = (): boolean => { @@ -106,8 +106,8 @@ export class CollectionBaseView extends React.Component { if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer Doc.GetProto(doc).annotationOn = this.props.Document; } - let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; - let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; + let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplateField ? this.extensionDoc : this.props.Document; + let targetField = (this.props.fieldExt || this.props.Document.isTemplateField) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; Doc.AddDocToList(targetDataDoc, targetField, doc); Doc.GetProto(doc).lastOpened = new DateField; return true; @@ -118,8 +118,8 @@ export class CollectionBaseView extends React.Component { let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); docView && SelectionManager.DeselectDoc(docView); //TODO This won't create the field if it doesn't already exist - let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; - let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; + let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplateField ? this.extensionDoc : this.props.Document; + let targetField = (this.props.fieldExt || this.props.Document.isTemplateField) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index cde1a5036..e54374ad7 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -110,6 +110,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } componentDidMount() { + super.componentDidMount(); this._heightDisposer = reaction(() => { if (this.props.Document.autoHeight) { let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); @@ -137,6 +138,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { ); } componentWillUnmount() { + super.componentWillUnmount(); this._heightDisposer && this._heightDisposer(); this._sectionFilterDisposer && this._sectionFilterDisposer(); } @@ -167,7 +169,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { renderDepth={this.props.renderDepth} ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} fitToBox={this.props.fitToBox} - onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler} + onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} PanelHeight={height} getTransform={finalDxf} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 9919a9dc3..46fbb5910 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -57,8 +57,13 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { componentDidMount() { this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => args[1] instanceof Doc && - this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc)))); + async (args) => { + if (args[1] instanceof Doc) { + this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc))); + } else { + this.childDocs.map(async doc => doc.layout = undefined); + } + }); } componentWillUnmount() { @@ -70,7 +75,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { // to its children which may be templates. // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise. @computed get dataField() { - return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey]; + return Doc.fieldExtensionDoc(this.props.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey]; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 403da0e54..0e9c38fb4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -520,7 +520,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; - @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplateField) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer && this.treedropDisposer(); @@ -530,6 +530,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } componentWillUnmount() { + super.componentWillUnmount(); this.treedropDisposer && this.treedropDisposer(); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 229e7fffc..2b0ef8ada 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -627,7 +627,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; - if (this.childDocs.some(d => BoolCast(d.isTemplate))) { + if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) { layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); } layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index 3cf8c3eb3..b4d33fb0f 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -32,7 +32,11 @@ export class ButtonBox extends DocComponent(Butt public static LayoutString() { return FieldView.LayoutString(ButtonBox); } private dropDisposer?: DragManager.DragDropDisposer; - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get dataDoc() { + return this.props.DataDoc && + (BoolCast(this.props.Document.isTemplateField) || BoolCast(this.props.DataDoc.isTemplateField) || + this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); + } protected createDropTarget = (ele: HTMLDivElement) => { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a071f782a..a7e78a7e8 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -110,7 +110,7 @@ export const documentSchema = createSchema({ dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents - isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed + isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) type: "string", // enumerated type of document maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) @@ -196,7 +196,7 @@ export class DocumentView extends DocComponent(Docu SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } else if (this.onClickHandler && this.onClickHandler.script) { - this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); } else if (this.props.Document.type === DocumentType.BUTTON) { ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY); } else if (this.Document.isButton) { @@ -376,7 +376,7 @@ export class DocumentView extends DocComponent(Docu @undoBatch @action freezeNativeDimensions = (): void => { - let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); + let proto = this.Document.isTemplateDoc ? this.props.Document : Doc.GetProto(this.props.Document); proto.autoHeight = this.Document.autoHeight = false; proto.ignoreAspect = !proto.ignoreAspect; if (!proto.ignoreAspect && !proto.nativeWidth) { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index f157a953e..ea82b1161 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -993,7 +993,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); + let nh = this.props.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.layoutDoc.height, 0); this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index aa9b28118..19968e6e1 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -323,7 +323,7 @@ export class VideoBox extends DocAnnotatableComponent(field.map((d: any) => Doc.MakeAlias(d))); }); Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index ef977c89a..5ce707011 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -36,11 +36,11 @@ export class CurrentUserUtils { // a default set of note types .. not being used yet... static setupNoteTypes(doc: Doc) { return [ - Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), - Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), - Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), - Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true }), - Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplate: true }) + Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplateDoc: true }), + Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplateDoc: true }), + Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplateDoc: true }), + Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplateDoc: true }), + Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplateDoc: true }) ]; } @@ -98,17 +98,17 @@ export class CurrentUserUtils { }); doc.documents = Docs.Create.TreeDocument([], { - title: "DOCUMENTS", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true + title: "DOCUMENTS", height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee" }); // setup Recently Closed library item doc.recentlyClosed = Docs.Create.TreeDocument([], { - title: "Recently Closed".toUpperCase(), height: 75, boxShadow: "0 0", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, backgroundColor: "#eeeeee" + title: "RECENTLY CLOSED", height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee" }); return Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library", - panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { + panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], { title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true }), targetContainer: sidebarContainer, -- cgit v1.2.3-70-g09d2 From caebaeca6993ff48609ab16dcc390d4d0458b03a Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 16 Oct 2019 23:50:27 -0400 Subject: fixed applying templates to collections. fixed templates containing collections --- src/client/documents/Documents.ts | 2 +- src/client/views/CollectionLinearView.tsx | 35 ++++++++++++++-------- src/client/views/InkingCanvas.tsx | 3 +- src/client/views/InkingControl.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- .../views/collections/CollectionViewChromes.tsx | 4 +-- src/client/views/nodes/ColorBox.tsx | 7 ++--- src/client/views/nodes/FormattedTextBox.tsx | 4 +-- src/new_fields/Doc.ts | 14 +++++++-- .../authentication/models/current_user_utils.ts | 14 +++------ 10 files changed, 51 insertions(+), 36 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2ffbc8394..62175cbe3 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -69,7 +69,7 @@ export interface DocumentOptions { preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view layout?: string | Doc; hideHeadings?: boolean; // whether stacking view column headings should be hidden - isTemplate?: boolean; + isTemplateField?: boolean; isTemplateDoc?: boolean; templates?: List; viewType?: number; diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 3e2ab1459..04e131135 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -47,24 +47,35 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { } } + makeTemplate = (doc: Doc): boolean => { + let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; + let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0]; + let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, ""); + let docs = DocListCast(layoutDoc[fieldKey]); + let any = false; + docs.map(d => { + if (!StrCast(d.title).startsWith("-")) { + any = true; + return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)); + } + if (d.type === DocumentType.COL) return this.makeTemplate(d); + return false; + }); + return any; + } + drop = action((e: Event, de: DragManager.DropEvent) => { (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { let dbox = doc; if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { - let template = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; - if (template.type === DocumentType.COL) { - let layout = StrCast(template.layout).match(/fieldKey={"[^"]*"}/)![0]; - let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, ""); - let docs = DocListCast(template[fieldKey]); - docs.map(d => { - Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(template)); - }); - template.isTemplateDoc = true; + let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; + if (layoutDoc.type === DocumentType.COL) { + layoutDoc.isTemplateDoc = this.makeTemplate(layoutDoc); } else { - template.isTemplateDoc = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction; + layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction; } - dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplateDoc ? "font" : "bolt" }); - dbox.dragFactory = template; + dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" }); + dbox.dragFactory = layoutDoc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); } else if (doc.viewType === CollectionViewType.Linear) { diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 9ab320eab..920ebaedd 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -181,9 +181,10 @@ export class InkingCanvas extends React.Component { render() { let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect"; + let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ? "pointer" : "default") : undefined; return (

    -
    +
    {this.props.children()} {this.drawnPaths}
    diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index bc5249acd..b8f3c2875 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -94,7 +94,7 @@ export class InkingControl { redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured) }); } else { - CurrentUserUtils.UserDocument.activePen instanceof Doc && CurrentUserUtils.UserDocument.activePen.pen instanceof Doc && (CurrentUserUtils.UserDocument.activePen.pen.backgroundColor = this._selectedColor); + CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor); } }); @action diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 46fbb5910..1f8c0b18f 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -61,7 +61,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { if (args[1] instanceof Doc) { this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc))); } else { - this.childDocs.map(async doc => doc.layout = undefined); + this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layout = undefined); } }); diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 3a66c05f4..a5b7f0181 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -40,9 +40,9 @@ export class CollectionViewBaseChrome extends React.Component this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined + immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; _contentCommand = { // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index e4fef0922..30554ea36 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -25,9 +25,8 @@ export class ColorBox extends DocStaticComponent( this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(), action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"), { fireImmediately: true }); - this._penDisposer = reaction(() => CurrentUserUtils.UserDocument.activePen instanceof Doc && CurrentUserUtils.UserDocument.activePen.pen, - action(() => this._startupColor = CurrentUserUtils.UserDocument.activePen instanceof Doc && CurrentUserUtils.UserDocument.activePen.pen instanceof Doc ? - StrCast(CurrentUserUtils.UserDocument.activePen.pen.backgroundColor, "black") : "black"), + this._penDisposer = reaction(() => CurrentUserUtils.ActivePen, + action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"), { fireImmediately: true }); // compare to this reaction that used to be in Selection Manager @@ -36,7 +35,7 @@ export class ColorBox extends DocStaticComponent( // if (sel.length > 0) { // let firstView = sel[0]; // let doc = firstView.props.Document; - // let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); + // let targetDoc = doc.isTemplateField ? doc : Doc.GetProto(doc); // let stored = StrCast(targetDoc.backgroundColor); // stored.length > 0 && (targetColor = stored); // } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index ea82b1161..045eee3f7 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -143,7 +143,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } // the document containing the view layout information - will be the Document itself unless the Document has // a layout field. In that case, all layout information comes from there unless overriden by Document @@ -265,7 +265,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } // apply as template when dragging with Meta } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") { - draggedDoc.isTemplate = true; + draggedDoc.isTemplateDoc = true; let newLayout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc; if (typeof (draggedDoc.layout) === "string") { newLayout = Doc.MakeDelegate(draggedDoc); diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 51fcb818a..04f310785 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -599,7 +599,7 @@ export namespace Doc { return target; } - export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false) { + export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false): boolean { // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??) let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, ""); let backgroundLayout = StrCast(fieldTemplate.backgroundLayout); @@ -613,7 +613,7 @@ export namespace Doc { fieldTemplate.templateField = metadataFieldName; fieldTemplate.title = metadataFieldName; - fieldTemplate.isTemplate = true; + fieldTemplate.isTemplateField = true; fieldTemplate.backgroundLayout = backgroundLayout; /* move certain layout properties from the original data doc to the template layout to avoid inheriting them from the template's data doc which may also define these fields for its own use. @@ -636,6 +636,7 @@ export namespace Doc { if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor; fieldTemplate.proto = templateDataDoc; }), 0); + return true; } export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) { @@ -735,9 +736,18 @@ export namespace Doc { export function UnBrushAllDocs() { manager.BrushedDoc.clear(); } + + export function setChildLayout(target: Doc, source?: Doc) { + target.childLayout = source && source.isTemplateDoc ? source : source && + source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory : + source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined; + } } + + Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; }); Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); +Scripting.addGlobal(function setChildLayout(target: any, source: any) { Doc.setChildLayout(target, source); }); Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); }); Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 5ce707011..e6f202685 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -23,16 +23,11 @@ export class CurrentUserUtils { public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } @computed public static get UserDocument() { return Doc.UserDoc(); } + @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).pen as Doc; } @observable public static GuestTarget: Doc | undefined; @observable public static GuestWorkspace: Doc | undefined; - private static createUserDocument(id: string): Doc { - let doc = new Doc(id, true); - doc.title = Doc.CurrentUserEmail; - return this.updateUserDocument(doc);// this should be the last - } - // a default set of note types .. not being used yet... static setupNoteTypes(doc: Doc) { return [ @@ -174,6 +169,7 @@ export class CurrentUserUtils { } static updateUserDocument(doc: Doc) { + doc.title = Doc.CurrentUserEmail; new InkingControl(); (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc); (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc); @@ -216,10 +212,8 @@ export class CurrentUserUtils { Doc.CurrentUserEmail = email; await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => { if (id && id !== "guest") { - return DocServer.GetRefField(id).then(async field => { - let userDoc = field instanceof Doc ? await this.updateUserDocument(field) : this.createUserDocument(id); - runInAction(() => Doc.SetUserDoc(userDoc)); - }); + return DocServer.GetRefField(id).then(async field => + Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true)))); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } -- cgit v1.2.3-70-g09d2 From 1f3576a69a6a0396d07e965c700bb7d69d77a0a3 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Fri, 18 Oct 2019 17:06:41 -0400 Subject: fixed up creator buttons - factored out convert to buttons code into a dropConverter script. fixed issues with template loading for images. --- src/client/documents/Documents.ts | 2 +- src/client/util/DragManager.ts | 3 ++ src/client/util/DropConverter.ts | 46 ++++++++++++++++++++++ src/client/views/CollectionLinearView.tsx | 40 ------------------- .../views/collections/CollectionStackingView.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 6 ++- .../views/collections/CollectionTreeView.scss | 1 - .../views/collections/CollectionTreeView.tsx | 2 +- .../CollectionFreeFormLinkView.tsx | 2 +- .../authentication/models/current_user_utils.ts | 11 ++++-- 10 files changed, 65 insertions(+), 50 deletions(-) create mode 100644 src/client/util/DropConverter.ts (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 651662ded..32fda1954 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -105,7 +105,7 @@ export interface DocumentOptions { yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts panel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script - convertToButtons?: boolean; // whether documents dropped onto a collection should be converted to buttons that will construct the dropped document + dropConverter?: ScriptField; // script to run when documents are dropped on this Document. // [key: string]: Opt; } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 576b16bc8..bbc29585c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -14,6 +14,8 @@ import { ScriptField } from "../../new_fields/ScriptField"; import { List } from "../../new_fields/List"; import { PrefetchProxy } from "../../new_fields/Proxy"; import { listSpec } from "../../new_fields/Schema"; +import { Scripting } from "./Scripting"; +import { convertDropDataToButtons } from "./DropConverter"; export type dropActionType = "alias" | "copy" | undefined; export function SetupDrag( @@ -497,3 +499,4 @@ export namespace DragManager { } } } +Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts new file mode 100644 index 000000000..eea3da1bc --- /dev/null +++ b/src/client/util/DropConverter.ts @@ -0,0 +1,46 @@ +import { DragManager } from "./DragManager"; +import { CollectionViewType } from "../views/collections/CollectionBaseView"; +import { Doc, DocListCast } from "../../new_fields/Doc"; +import { DocumentType } from "../documents/DocumentTypes"; +import { ObjectField } from "../../new_fields/ObjectField"; +import { StrCast } from "../../new_fields/Types"; +import { Docs } from "../documents/Documents"; +import { ScriptField } from "../../new_fields/ScriptField"; + + +function makeTemplate(doc: Doc): boolean { + let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; + let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0]; + let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, ""); + let docs = DocListCast(layoutDoc[fieldKey]); + let any = false; + docs.map(d => { + if (!StrCast(d.title).startsWith("-")) { + any = true; + return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)); + } + if (d.type === DocumentType.COL) return makeTemplate(d); + return false; + }); + return any; +} +export function convertDropDataToButtons(data: DragManager.DocumentDragData) { + data && data.draggedDocuments.map((doc, i) => { + let dbox = doc; + if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) { + let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; + if (layoutDoc.type === DocumentType.COL) { + layoutDoc.isTemplateDoc = makeTemplate(layoutDoc); + } else { + layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && !data.userDropAction; + } + dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" }); + dbox.dragFactory = layoutDoc; + dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; + dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); + } else if (doc.viewType === CollectionViewType.Linear) { + dbox.ignoreClick = true; + } + data.droppedDocuments[i] = dbox; + }); +} diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 04e131135..e8ef20899 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -47,46 +47,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { } } - makeTemplate = (doc: Doc): boolean => { - let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; - let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0]; - let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, ""); - let docs = DocListCast(layoutDoc[fieldKey]); - let any = false; - docs.map(d => { - if (!StrCast(d.title).startsWith("-")) { - any = true; - return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)); - } - if (d.type === DocumentType.COL) return this.makeTemplate(d); - return false; - }); - return any; - } - - drop = action((e: Event, de: DragManager.DropEvent) => { - (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => { - let dbox = doc; - if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) { - let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; - if (layoutDoc.type === DocumentType.COL) { - layoutDoc.isTemplateDoc = this.makeTemplate(layoutDoc); - } else { - layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction; - } - dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" }); - dbox.dragFactory = layoutDoc; - dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; - dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); - } else if (doc.viewType === CollectionViewType.Linear) { - dbox.ignoreClick = true; - } - (de.data as DragManager.DocumentDragData).droppedDocuments[i] = dbox; - }); - e.stopPropagation(); - return super.drop(e, de); - }); - public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } dimension = () => NumCast(this.props.Document.height); // 2 * the padding diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index e54374ad7..f9f040c6b 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -65,7 +65,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return
    - {this.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)} + {pair.layout instanceof Doc && this.getDisplayDoc(pair.layout, pair.data, dxf, width)}
    ; }); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 1f8c0b18f..bc61492d0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -6,7 +6,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; @@ -23,6 +23,8 @@ import React = require("react"); var path = require('path'); import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; +import { CollectionViewType } from "./CollectionBaseView"; +import { ObjectField } from "../../../new_fields/ObjectField"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -126,6 +128,8 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { + (this.props.Document.dropConverter instanceof ScriptField) && + this.props.Document.dropConverter.script.run({ dragData: de.data }); if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { if (de.mods === "AltKey" && de.data.draggedDocuments.length) { this.childDocs.map(doc => diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index bff8ce5c1..7d0c900a6 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -10,7 +10,6 @@ width:100%; position: relative; top:0; - padding-top: 20px; padding-left: 10px; padding-right: 10px; background: $light-color-secondary; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 2c77c4b5b..5d88e6290 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -576,7 +576,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : (
    this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index c5bc2e7fb..962fe2a1c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -31,7 +31,7 @@ export class CollectionFreeFormLinkView extends React.Component(["dropAction"]), dragFactory: data.dragFactory + backgroundColor: data.backgroundColor, removeDropProperties: new List(["dropAction"]), dragFactory: data.dragFactory, })); } @@ -69,7 +70,8 @@ export class CurrentUserUtils { static setupCreatePanel(sidebarContainer: Doc, doc: Doc) { // setup a masonry view of all he creators const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), { - width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" + width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 0 }); // setup a color picker const color = Docs.Create.ColorDocument({ @@ -81,7 +83,7 @@ export class CurrentUserUtils { panel: Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" }), - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel") + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel"), }); } @@ -148,7 +150,8 @@ export class CurrentUserUtils { doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], { title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", - backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true, + backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) }); } -- cgit v1.2.3-70-g09d2 From a1726e5a20653f6239b2ef2cb4de7816f0854855 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 19 Oct 2019 10:49:51 -0400 Subject: repaired uploading of image audio annotations --- src/client/views/nodes/ImageBox.tsx | 2 +- src/server/index.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'src/server') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9f39eccea..5bca8b7cf 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -157,7 +157,7 @@ export class ImageBox extends DocAnnotatableComponent Date: Sat, 19 Oct 2019 10:53:43 -0400 Subject: from last. --- src/server/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src/server') diff --git a/src/server/index.ts b/src/server/index.ts index 87b7ad76d..ee6a497ba 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -746,9 +746,7 @@ addSecureRoute({ }); }); } else if (type.indexOf("audio") !== -1) { - if (location !== uploadDirectory + filename) { - fs.createReadStream(uploadDirectory + filename).pipe(fs.createWriteStream(location)); - } + // nothing to be done yet-- although transcribing the audio a la pdfs would make sense. } else { uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename); } -- cgit v1.2.3-70-g09d2 From d8f12dade0d0b336e4328fa1f00a1a538588d34e Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sun, 20 Oct 2019 13:34:25 -0400 Subject: initial version of template refactoring --- src/client/documents/Documents.ts | 10 +-- src/client/views/DocumentDecorations.tsx | 26 ++++---- src/client/views/TemplateMenu.tsx | 3 +- .../views/collections/CollectionDockingView.tsx | 23 +++---- .../views/collections/CollectionSchemaView.tsx | 13 ++-- .../views/collections/CollectionStackingView.tsx | 37 +++++++---- src/client/views/collections/CollectionSubView.tsx | 11 ++-- .../views/collections/CollectionTreeView.tsx | 19 +++--- .../CollectionFreeFormLayoutEngines.tsx | 3 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 4 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- src/client/views/nodes/DocumentContentsView.tsx | 11 ++-- src/client/views/nodes/DocumentView.tsx | 77 +++++++--------------- src/client/views/nodes/FormattedTextBox.tsx | 3 +- src/client/views/nodes/ImageBox.tsx | 52 ++++++++------- src/client/views/nodes/PDFBox.tsx | 10 +-- src/client/views/nodes/VideoBox.tsx | 35 +++++----- src/client/views/nodes/WebBox.tsx | 11 ++-- src/client/views/pdf/PDFViewer.tsx | 40 +++++------ .../views/presentationview/PresElementBox.tsx | 9 +-- src/client/views/search/SearchBox.tsx | 2 +- src/client/views/search/SearchItem.tsx | 2 +- src/new_fields/Doc.ts | 37 +++++------ .../authentication/models/current_user_utils.ts | 2 +- 24 files changed, 211 insertions(+), 231 deletions(-) (limited to 'src/server') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 32fda1954..fbf45816a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -300,7 +300,7 @@ export namespace Docs { */ export namespace Create { - const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "dropAction", "forceActive"]; + const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "nativeWidth", "nativeHeight", "dropAction", "forceActive", "fitWidth"]; /** * This function receives the relevant document prototype and uses @@ -369,11 +369,11 @@ export namespace Docs { requestImageSize(target) .then((size: any) => { let aspect = size.height / size.width; - if (!inst.proto!.nativeWidth) { - inst.proto!.nativeWidth = size.width; + if (!inst.nativeWidth) { + inst.nativeWidth = size.width; } - inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect; - inst.proto!.height = NumCast(inst.proto!.width) * aspect; + inst.nativeHeight = NumCast(inst.nativeWidth) * aspect; + inst.height = NumCast(inst.width) * aspect; }) .catch((err: any) => console.log(err)); // } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 07af4799b..252f90d46 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -19,7 +19,7 @@ import { CollectionView } from "./collections/CollectionView"; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { PositionDocument } from './nodes/CollectionFreeFormDocumentView'; -import { DocumentView, swapViews } from "./nodes/DocumentView"; +import { DocumentView } from "./nodes/DocumentView"; import { FieldView } from "./nodes/FieldView"; import { FormattedTextBox } from "./nodes/FormattedTextBox"; import { IconBox } from "./nodes/IconBox"; @@ -107,10 +107,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> fieldTemplate.title = metaKey; Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate)); if (text.startsWith(">>")) { - let layoutNative = Doc.MakeTitled("layoutNative"); - Doc.GetProto(docTemplate).layoutNative = layoutNative; - swapViews(fieldTemplate, "", "layoutNative", layoutNative); - layoutNative.layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); + Doc.GetProto(docTemplate).layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); } } } @@ -471,8 +468,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { let doc = PositionDocument(element.props.Document); let layoutDoc = PositionDocument(Doc.Layout(element.props.Document)); - let nwidth = doc.nativeWidth || 0; - let nheight = doc.nativeHeight || 0; + let nwidth = layoutDoc.nativeWidth || 0; + let nheight = layoutDoc.nativeHeight || 0; let width = (layoutDoc.width || 0); let height = (layoutDoc.height || (nheight / nwidth * width)); let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); @@ -480,21 +477,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let actualdH = Math.max(height + (dH * scale), 20); doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); - let proto = doc.isTemplateField ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here... let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight); if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) { layoutDoc.ignoreAspect = false; - proto.nativeWidth = nwidth = layoutDoc.width || 0; - proto.nativeHeight = nheight = layoutDoc.height || 0; + layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; + layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; } if (fixedAspect && (!nwidth || !nheight)) { - proto.nativeWidth = nwidth = layoutDoc.width || 0; - proto.nativeHeight = nheight = layoutDoc.height || 0; + layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; + layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; } if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) { if (Math.abs(dW) > Math.abs(dH)) { if (!fixedAspect) { - Doc.SetInPlace(doc, "nativeWidth", actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0), true); + layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0); } layoutDoc.width = actualdW; if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width; @@ -502,7 +498,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } else { if (!fixedAspect) { - Doc.SetInPlace(doc, "nativeHeight", actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0), true); + layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0); } layoutDoc.height = actualdH; if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height; @@ -511,7 +507,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } else { dW && (layoutDoc.width = actualdW); dH && (layoutDoc.height = actualdH); - dH && layoutDoc.autoHeight && Doc.SetInPlace(layoutDoc, "autoHeight", false, true); + dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false); } } }); diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index d76b033f0..96265385e 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -9,6 +9,7 @@ import { DocumentView } from "./nodes/DocumentView"; import { Template, Templates } from "./Templates"; import React = require("react"); import { Doc } from "../../new_fields/Doc"; +import { StrCast } from "../../new_fields/Types"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -128,7 +129,7 @@ export class TemplateMenu extends React.Component { this.props.templates.forEach((checked, template) => templateMenu.push()); templateMenu.push(); - templateMenu.push(); + templateMenu.push(); templateMenu.push(); return (
    diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1f78c8c97..42d372f4a 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -615,19 +615,20 @@ export class DockedFrameRenderer extends React.Component { } } - panelWidth = () => this._document && this._document.maxWidth ? Math.min(Math.max(NumCast(this._document.width), NumCast(this._document.nativeWidth)), this._panelWidth) : this._panelWidth; + get layoutDoc() { return this._document && Doc.Layout(this._document);} + panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; - nativeWidth = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0; contentScaling = () => { - if (this._document!.type === DocumentType.PDF) { - if ((this._document && this._document.fitWidth) || - this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) { - return this._panelWidth / NumCast(this._document!.nativeWidth); + if (this.layoutDoc!.type === DocumentType.PDF) { + if ((this.layoutDoc && this.layoutDoc.fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) { + return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth); } else { - return this._panelHeight / NumCast(this._document!.nativeHeight); + return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight); } } const nativeH = this.nativeHeight(); @@ -645,7 +646,7 @@ export class DockedFrameRenderer extends React.Component { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth() && !this._document!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } addDocTab = (doc: Doc, dataDoc: Opt, location: string) => { SelectionManager.DeselectAll(); @@ -690,11 +691,11 @@ export class DockedFrameRenderer extends React.Component { } render() { - return (!this._isActive || !this._document) ? (null) : + return (!this._isActive || !this.layoutDoc) ? (null) : (
    this._mainCont = ref} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this._document && this._document.fitWidth ? undefined : "100%" + height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%" }}> {this.docView}
    ); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 3218f630a..23e070750 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -922,8 +922,9 @@ interface CollectionSchemaPreviewProps { export class CollectionSchemaPreview extends React.Component{ private dropDisposer?: DragManager.DragDropDisposer; _mainCont?: HTMLDivElement; - private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.PanelWidth()); } - private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.PanelHeight()); } + private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); } + private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); } + private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); } private contentScaling = () => { let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth()); if (wscale * this.nativeHeight > this.props.PanelHeight()) { @@ -947,10 +948,8 @@ export class CollectionSchemaPreview extends React.Component { let target = Doc.GetProto(otherdoc); - let layoutNative = Doc.MakeTitled("layoutNative"); - layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]"); - target.layoutNative = layoutNative; - target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]); + target.layout = ComputedField.MakeFunction("this.image_data[0]"); + target.layoutCustom = Doc.MakeDelegate(de.data.draggedDocuments[0]); }); e.stopPropagation(); } @@ -968,7 +967,7 @@ export class CollectionSchemaPreview extends React.Component doc) { this._docXfs.length = 0; return docs.map((d, i) => { let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); - let height = () => this.getDocHeight(pair.layout); + let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d; + let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + let height = () => this.getDocHeight(layoutDoc); let dref = React.createRef(); - let dxf = () => this.getDocTransform(pair.layout!, dref.current!); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return
    - {pair.layout instanceof Doc && this.getDisplayDoc(pair.layout, pair.data, dxf, width)} + {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
    ; }); } @@ -111,8 +112,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (this.props.Document.autoHeight) { let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); if (this.isStackingView) { - return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght, - (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)), 0); + let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { + let r1 = Math.max(maxHght, + (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => { + let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); + return val; + }, this.yMargin)); + return r1; + }, 0); + return res; } else { let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); return this.props.ContentScaling() * (sum + (this.Sections.size ? 85 : -15)); @@ -122,7 +130,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }, (hgt: number) => { let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; - doc && (doc.height = hgt); + doc && (Doc.Layout(doc).height = hgt); }, { fireImmediately: true } ); @@ -155,11 +163,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } - getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { - let height = () => this.getDocHeight(layoutDoc); + getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { + let layoutDoc = Doc.Layout(doc); + let height = () => this.getDocHeight(doc); let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); return doc) { getDocHeight(d?: Doc) { if (!d) return 0; let layoutDoc = Doc.Layout(d); - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); + let nw = NumCast(layoutDoc.nativeWidth); + let nh = NumCast(layoutDoc.nativeHeight); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) { let aspect = nw && nh ? nh / nw : 1; if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return layoutDoc.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); + return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index f76447157..0e8c46d40 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -61,11 +61,11 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], async (args) => { if (args[1] instanceof Doc) { - this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc))); + this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent")); + } + else if (!(args[1] instanceof Promise)) { + this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout")); } - // else if (!(args[1] instanceof Promise)) { - // this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layout = undefined); - // } }); } @@ -134,8 +134,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { if (de.mods === "AltKey" && de.data.draggedDocuments.length) { this.childDocs.map(doc => - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc) - ); + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, "layoutFromParent")); e.stopPropagation(); return true; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 2fbe8527e..77736b7d0 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -249,23 +249,22 @@ class TreeView extends React.Component { let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); return finalXf; } + get layoutDoc() { return Doc.Layout(this.props.document); } docWidth = () => { - let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); - let layoutDoc = Doc.Layout(this.props.document); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(this.props.document.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + let aspect = NumCast(this.layoutDoc.nativeHeight) / NumCast(this.layoutDoc.nativeWidth); + if (aspect) return Math.min(this.layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); + return NumCast(this.layoutDoc.nativeWidth) ? Math.min(this.layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { let bounds = this.boundsOfCollectionDocument; return Math.min(this.MAX_EMBED_HEIGHT, (() => { - let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); - let layoutDoc = Doc.Layout(this.props.document); + let aspect = NumCast(this.layoutDoc.nativeHeight) / NumCast(this.layoutDoc.nativeWidth); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth, + return this.layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : + Math.min(this.docWidth() * NumCast(this.layoutDoc.scrollHeight, NumCast(this.layoutDoc.nativeHeight)) / NumCast(this.layoutDoc.nativeWidth, NumCast(this.props.containingCollection.height)))) : - NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50; + NumCast(this.layoutDoc.height) ? NumCast(this.layoutDoc.height) : 50; })()); } @@ -484,7 +483,7 @@ class TreeView extends React.Component { }; const childLayout = Doc.Layout(pair.layout); let rowHeight = () => { - let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); + let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0); return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); }; return = numCols) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index eff73b14e..aa1106f13 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -63,10 +63,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _hitCluster = false; @observable _clusterSets: (Doc[])[] = []; + get layoutDoc() { return PanZoomDocument(Doc.Layout(this.props.Document)); } @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } - @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; } + @computed get nativeWidth() { return this.layoutDoc.fitToContent ? 0 : this.layoutDoc.nativeWidth || 0; } @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } @@ -448,6 +449,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ...this.props, DataDoc: childData, Document: childLayout, + layoutKey: undefined, ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 2243a44d5..e3ca02fa4 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -77,7 +77,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; - let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined; + let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined; let br = StrCast((ld || this.props.Document).borderRounding); br = !br && ruleRounding ? ruleRounding : br; if (br.endsWith("%")) { diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index bb9315ca3..7aed416f4 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -2,7 +2,7 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../new_fields/Doc"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { OmitKeys, Without } from "../../../Utils"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; @@ -69,10 +69,9 @@ export class DocumentContentsView extends React.Component obj.active = this.props.parentActive).omit, Document: this.layoutDoc, - DataDoc: this.dataDoc + DataDoc: this.dataDoc, }; return { props: list }; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1087c57ad..2d13eeae4 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -191,9 +191,8 @@ export class DocumentView extends DocComponent(Docu let preventDefault = true; if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !this.onClickHandler.script)) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click let fullScreenAlias = Doc.MakeAlias(this.props.Document); - let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); - if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { - await swapViews(fullScreenAlias, "layoutCustom", "layoutNative"); + if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias["layoutCustom"] !== undefined) { + fullScreenAlias.layoutKey = "layoutCustom"; } this.props.addDocTab(fullScreenAlias, undefined, "inTab"); SelectionManager.DeselectAll(); @@ -287,16 +286,13 @@ export class DocumentView extends DocComponent(Docu @undoBatch deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } - static makeNativeViewClicked = async (doc: Doc): Promise => { - undoBatch(() => swapViews(doc, "layoutNative", "layoutCustom"))(); + static makeNativeViewClicked = (doc: Doc) => { + undoBatch(() => doc.layoutKey = "layout")(); } - static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt) => { + static makeCustomViewClicked = (doc: Doc, dataDoc: Opt) => { const batch = UndoManager.StartBatch("CustomViewClicked"); if (doc.layoutCustom === undefined) { - Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative"); - await swapViews(doc, "", "layoutNative"); - const width = NumCast(doc.width); const height = NumCast(doc.height); const options = { title: "data", width, x: -width / 2, y: - height / 2, }; @@ -326,10 +322,9 @@ export class DocumentView extends DocComponent(Docu let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) }); Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true); - Doc.ApplyTemplateTo(docTemplate, doc, undefined); - Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom"); + Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined); } else { - await swapViews(doc, "layoutCustom", "layoutNative"); + doc.layoutKey = "layoutCustom"; } batch.end(); } @@ -356,7 +351,7 @@ export class DocumentView extends DocComponent(Docu DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`); } if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document); + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, "layoutCustom"); e.stopPropagation(); } if (de.data instanceof DragManager.LinkDragData) { @@ -383,12 +378,11 @@ export class DocumentView extends DocComponent(Docu @undoBatch @action freezeNativeDimensions = (): void => { - let proto = this.Document.isTemplateDoc ? this.props.Document : Doc.GetProto(this.props.Document); - proto.autoHeight = this.Document.autoHeight = false; - proto.ignoreAspect = !proto.ignoreAspect; - if (!proto.ignoreAspect && !proto.nativeWidth) { - proto.nativeWidth = this.props.PanelWidth(); - proto.nativeHeight = this.props.PanelHeight(); + this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false; + this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect; + if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) { + this.layoutDoc.nativeWidth = this.props.PanelWidth(); + this.layoutDoc.nativeHeight = this.props.PanelHeight(); } } @@ -505,7 +499,7 @@ export class DocumentView extends DocComponent(Docu layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) { layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - } else if (this.props.Document.layoutNative) { + } else { layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); @@ -599,7 +593,7 @@ export class DocumentView extends DocComponent(Docu return (showTitle ? 25 : 0) + 1; } - childScaling = () => (this.props.Document.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); + childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { return ((Docu this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); - const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; - const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%"; + const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); @@ -686,7 +680,7 @@ export class DocumentView extends DocComponent(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; - let highlighting = fullDegree && this.props.Document.type !== DocumentType.FONTICON && this.props.Document.viewType !== CollectionViewType.Linear; + let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; return (
    (Docu color: StrCast(this.Document.color), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor, + background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, - transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, + transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`, opacity: this.Document.opacity }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} @@ -707,7 +701,7 @@ export class DocumentView extends DocComponent(Docu > {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) => //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) : -
    +
    )} {!showTitle && !showCaption ? @@ -733,33 +727,6 @@ export class DocumentView extends DocComponent(Docu } } -export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) { - let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc); - if (oldLayoutExt) { - oldLayoutExt.autoHeight = doc.autoHeight; - oldLayoutExt.width = doc.width; - oldLayoutExt.height = doc.height; - oldLayoutExt.nativeWidth = doc.nativeWidth; - oldLayoutExt.nativeHeight = doc.nativeHeight; - oldLayoutExt.ignoreAspect = doc.ignoreAspect; - oldLayoutExt.type = doc.type; - oldLayoutExt.layout = doc.layout; - } - - let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc); - if (newLayoutExt) { - doc.autoHeight = newLayoutExt.autoHeight; - doc.width = newLayoutExt.width; - doc.height = newLayoutExt.height; - doc.nativeWidth = newLayoutExt.nativeWidth; - doc.nativeHeight = newLayoutExt.nativeHeight; - doc.ignoreAspect = newLayoutExt.ignoreAspect; - doc.type = newLayoutExt.type; - doc.layout = await newLayoutExt.layout; - } -} - Scripting.addGlobal(function toggleDetail(doc: any) { - let native = typeof doc.layout === "string"; - swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom"); + doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; }); \ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index ac10d729a..f866e6f7e 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -271,7 +271,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe newLayout = Doc.MakeDelegate(draggedDoc); newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`); } - this.props.Document.layout = newLayout; + this.props.Document.layoutCustom = newLayout; + this.props.Document.layoutKey = "layoutCustom"; e.stopPropagation(); // embed document when dragging with a userDropAction or an embedDoc flag set } else if (de.data.userDropAction || de.data.embedDoc) { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2fc4c04e6..dc80af268 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -2,7 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app @@ -13,7 +13,7 @@ import { ComputedField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; import { AudioField, ImageField } from '../../../new_fields/URLField'; import { RouteStore } from '../../../server/RouteStore'; -import { Utils, returnOne, emptyFunction } from '../../../Utils'; +import { Utils, returnOne, emptyFunction, OmitKeys } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; @@ -159,7 +159,7 @@ export class ImageBox extends DocAnnotatableComponent { - let proto = Doc.GetProto(this.props.Document); - let nw = this.props.Document.nativeWidth; - let nh = this.props.Document.nativeHeight; - let w = this.props.Document.width; - let h = this.props.Document.height; - proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360; - proto.nativeWidth = nh; - proto.nativeHeight = nw; - this.props.Document.width = h; - this.props.Document.height = w; + let nw = this.layoutDoc.nativeWidth; + let nh = this.layoutDoc.nativeHeight; + let w = this.layoutDoc.width; + let h = this.layoutDoc.height; + this.layoutDoc.rotation = (NumCast(this.layoutDoc.rotation) + 90) % 360; + this.layoutDoc.nativeWidth = nh; + this.layoutDoc.nativeHeight = nw; + this.layoutDoc.width = h; + this.layoutDoc.height = w; }); specificContextMenu = (e: React.MouseEvent): void => { @@ -251,7 +251,7 @@ export class ImageBox extends DocAnnotatableComponent @@ -379,16 +379,17 @@ export class ImageBox extends DocAnnotatableComponent 20) { @@ -401,14 +402,14 @@ export class ImageBox extends DocAnnotatableComponent - {() => [this.content]} + {() => [this.content(this.layoutDoc)]}
    ); } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index f31128356..f3ea8328b 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import 'react-image-lightbox/style.css'; -import { Opt, WidthSym } from "../../../new_fields/Doc"; +import { Opt, WidthSym, Doc } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; import { Cast } from "../../../new_fields/Types"; @@ -65,11 +65,13 @@ export class PDFBox extends DocAnnotatableComponent }, { fireImmediately: true }); } + get layoutDoc() { return Doc.Layout(this.props.Document); } + loaded = (nw: number, nh: number, np: number) => { this.dataDoc.numPages = np; - this.Document.nativeWidth = nw * 96 / 72; - this.Document.nativeHeight = nh * 96 / 72; - !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw)); + this.layoutDoc.nativeWidth = nw * 96 / 72; + this.layoutDoc.nativeHeight = nh * 96 / 72; + !this.layoutDoc.fitWidth && !this.layoutDoc.ignoreAspect && (this.layoutDoc.height = this.layoutDoc[WidthSym]() * (nh / nw)); } public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 19968e6e1..4c01279ae 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -55,14 +55,15 @@ export class VideoBox extends DocAnnotatableComponent { let aspect = this.player!.videoWidth / this.player!.videoHeight; - var nativeWidth = FieldValue(this.Document.nativeWidth, 0); - var nativeHeight = FieldValue(this.Document.nativeHeight, 0); + var nativeWidth = NumCast(this.layoutDoc.nativeWidth, 0); + var nativeHeight = NumCast(this.layoutDoc.nativeHeight, 0); if (!nativeWidth || !nativeHeight) { - if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth; - this.Document.nativeHeight = this.Document.nativeWidth / aspect; - this.Document.height = FieldValue(this.Document.width, 0) / aspect; + if (!this.layoutDoc.nativeWidth) this.layoutDoc.nativeWidth = this.player!.videoWidth; + this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / aspect; + this.layoutDoc.height = NumCast(this.layoutDoc.width, 0) / aspect; } if (!this.Document.duration) this.Document.duration = this.player!.duration; } @@ -107,7 +108,7 @@ export class VideoBox extends DocAnnotatableComponent {"" + Math.round(curTime)} {" " + Math.round((curTime - Math.trunc(curTime)) * 100)} @@ -312,7 +313,7 @@ export class VideoBox extends DocAnnotatableComponent { this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY); - this.Seek(Math.max(0, NumCast(this.props.Document.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333)); + this.Seek(Math.max(0, NumCast(this.layoutDoc.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333)); e.stopImmediatePropagation(); } @action @@ -320,10 +321,10 @@ export class VideoBox extends DocAnnotatableComponent Math.round(this.Document.currentTimecode || 0)); return