diff options
5 files changed, 421 insertions, 2 deletions
diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss new file mode 100644 index 000000000..8dd5cec03 --- /dev/null +++ b/src/client/views/PropertiesButtons.scss @@ -0,0 +1,105 @@ +@import "globalCssVariables"; + +$linkGap : 3px; + +.propertiesButtons-linkFlyout { + grid-column: 2/4; +} + +.propertiesButtons-linkButton-empty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.propertiesButtons-linkButton-nonempty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.propertiesButtons-linkButton-empty, +.propertiesButtons-linkButton-nonempty { + height: 20px; + width: 20px; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.propertiesButtons { + margin-top: $linkGap; + grid-column: 1/4; + width: max-content; + height: auto; + display: flex; + flex-direction: row; +} + +.propertiesButtons-button { + pointer-events: auto; + padding-right: 5px; + width: 25px; +} + +.propertiesButtons-linker { + height: 20px; + width: 20px; + text-align: center; + border-radius: 50%; + pointer-events: auto; + color: $dark-color; + border: $dark-color 1px solid; + transition: 0.2s ease all; +} + +.propertiesButtons-linker:hover { + cursor: pointer; + transform: scale(1.05); +} + + +@-moz-keyframes spin { + 100% { + -moz-transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8); + } + + 100% { + box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); + } +}
\ No newline at end of file diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx new file mode 100644 index 000000000..842d0320f --- /dev/null +++ b/src/client/views/PropertiesButtons.tsx @@ -0,0 +1,296 @@ +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { RichTextField } from '../../fields/RichTextField'; +import { Cast, NumCast } from "../../fields/Types"; +import { emptyFunction, setupMoveUpEvents } from "../../Utils"; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { Docs, DocUtils } from '../documents/Documents'; +import { DragManager } from '../util/DragManager'; +import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; +import { ParentDocSelector } from './collections/ParentDocumentSelector'; +import './collections/ParentDocumentSelector.scss'; +import './PropertiesButtons.scss'; +import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { DocumentView } from './nodes/DocumentView'; +import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; +import { TemplateMenu } from "./TemplateMenu"; +import { Template, Templates } from "./Templates"; +import React = require("react"); +import { Tooltip } from '@material-ui/core'; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +library.add(faLink); +library.add(faTag); +library.add(faTimes); +library.add(faArrowAltCircleDown); +library.add(faArrowAltCircleUp); +library.add(faArrowAltCircleRight); +library.add(faStopCircle); +library.add(faCheckCircle); +library.add(faCloudUploadAlt); +library.add(faSyncAlt); +library.add(faShare); +library.add(faPhotoVideo); + +const cloud: IconProp = "cloud-upload-alt"; +const fetch: IconProp = "sync-alt"; + +enum UtilityButtonState { + Default, + OpenRight, + OpenExternally +} + +@observer +export class PropertiesButtons extends React.Component<{ doc: Doc }, {}> { + private _dragRef = React.createRef<HTMLDivElement>(); + private _pullAnimating = false; + private _pushAnimating = false; + private _pullColorAnimating = false; + + @observable private pushIcon: IconProp = "arrow-alt-circle-up"; + @observable private pullIcon: IconProp = "arrow-alt-circle-down"; + @observable private pullColor: string = "white"; + @observable public isAnimatingFetch = false; + @observable public isAnimatingPulse = false; + + @observable private openHover: UtilityButtonState = UtilityButtonState.Default; + + @observable public static Instance: PropertiesButtons; + public static hasPushedHack = false; + public static hasPulledHack = false; + + public startPullOutcome = action((success: boolean) => { + if (!this._pullAnimating) { + this._pullAnimating = true; + this.pullIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pullIcon = "arrow-alt-circle-down"; + this._pullAnimating = false; + }), 1000); + } + }); + + public startPushOutcome = action((success: boolean) => { + this.isAnimatingPulse = false; + if (!this._pushAnimating) { + this._pushAnimating = true; + this.pushIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pushIcon = "arrow-alt-circle-up"; + this._pushAnimating = false; + }), 1000); + } + }); + + public setPullState = action((unchanged: boolean) => { + this.isAnimatingFetch = false; + if (!this._pullColorAnimating) { + this._pullColorAnimating = true; + this.pullColor = unchanged ? "lawngreen" : "red"; + setTimeout(this.clearPullColor, 1000); + } + }); + + private clearPullColor = action(() => { + this.pullColor = "white"; + this._pullColorAnimating = false; + }); + + //get view0() { return this.props.views()?.[0]; } + + @computed + get considerGoogleDocsPush() { + const targetDoc = this.props.doc; + const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined; + const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none"; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}> + <div + className="propertiesButtons-linker" + style={{ animation }} + onClick={async () => { + await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + !published && runInAction(() => this.isAnimatingPulse = true); + PropertiesButtons.hasPushedHack = false; + targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} /> + </div></Tooltip>; + } + + @computed + get considerGoogleDocsPull() { + const targetDoc = this.props.doc; + const dataDoc = targetDoc && Doc.GetProto(targetDoc); + const animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; + + const title = (() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return `${!dataDoc?.unchanged ? "Pull from" : "Fetch"} Google Docs`; + case UtilityButtonState.OpenRight: return "Open in Right Split"; + case UtilityButtonState.OpenExternally: return "Open in new Browser Tab"; + } + })(); + + return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip + title={<><div className="dash-tooltip">{title}</div></>}> + <div className="propertiesButtons-linker" + style={{ backgroundColor: this.pullColor }} + onPointerEnter={action(e => { + if (e.altKey) { + this.openHover = UtilityButtonState.OpenExternally; + } else if (e.shiftKey) { + this.openHover = UtilityButtonState.OpenRight; + } + })} + onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} + onClick={async e => { + const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; + if (e.shiftKey) { + e.preventDefault(); + let googleDoc = await Cast(dataDoc.googleDoc, Doc); + if (!googleDoc) { + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + googleDoc = Docs.Create.WebDocument(googleDocUrl, options); + dataDoc.googleDoc = googleDoc; + } + CollectionDockingView.AddRightSplit(googleDoc); + } else if (e.altKey) { + e.preventDefault(); + window.open(googleDocUrl); + } else { + this.clearPullColor(); + PropertiesButtons.hasPulledHack = false; + targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + } + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" + style={{ WebkitAnimation: animation, MozAnimation: animation }} + icon={(() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; + case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; + case UtilityButtonState.OpenExternally: return "share"; + } + })()} + /> + </div></Tooltip>; + } + @computed + get pinButton() { + const targetDoc = this.props.doc; + const isPinned = targetDoc && Doc.isDocPinned(targetDoc); + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> + <div className="propertiesButtons-linker" + style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} + onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" + /> + </div></Tooltip>; + } + + @computed + get metadataButton() { + //const view0 = this.view0; + return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={<MetadataEntryMenu docs={[this.props.doc]} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> + <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; + } + + // @computed + // get contextButton() { + // return <ParentDocSelector Document={this.Document} addDocTab={(doc, where) => { + // where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : + // this.props.doc.props.addDocTab(doc, "onRight"); + // return true; + // }} />; + // } + + @observable _aliasDown = false; + onAliasButtonDown = (e: React.PointerEvent): void => { + //setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + + } + + // onAliasButtonMoved = () => { + // if (this._dragRef.current) { + // const dragDocView = this.props.doc!; + // if (dragDocView.props){ + // const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); + // const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + // dragData.dropAction = "alias"; + // DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { + // offsetX: dragData.offset[0], + // offsetY: dragData.offset[1], + // hideSource: false + // }); + // return true; + // } + + // } + // return false; + // } + + // @computed + // get templateButton() { + // //const view0 = this.view0; + // const templates: Map<Template, boolean> = new Map(); + // //const views = this.props.views(); + // Array.from(Object.values(Templates.TemplateList)).map(template => + // templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); + // return !this.props.doc ? (null) : + // <Tooltip title={<><div className="dash-tooltip">Tap: Customize layout. Drag: Create alias</div></>}> + // <div className="propertiesButtons-linkFlyout" ref={this._dragRef}> + // <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} + // content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + // <div className={"propertiesButtons-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > + // {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + // </div> + // </Flyout> + // </div></Tooltip>; + // } + + render() { + if (!this.props.doc) return (null); + + const isText = this.props.doc[Doc.LayoutFieldKey(this.props.doc)] instanceof RichTextField; + const considerPull = isText && this.considerGoogleDocsPull; + const considerPush = isText && this.considerGoogleDocsPush; + return <div className="propertiesButtons"> + {/* <div className="propertiesButtons-button"> + {this.templateButton} + </div> */} + <div className="propertiesButtons-button"> + {this.metadataButton} + </div> + {/* <div className="propertiesButtons-button"> + {this.contextButton} + </div> */} + <div className="propertiesButtons-button"> + {this.pinButton} + </div> + <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}> + {this.considerGoogleDocsPush} + </div> + <div className="propertiesButtons-button" style={{ display: !considerPull ? "none" : "" }}> + {this.considerGoogleDocsPull} + </div> + </div>; + } +} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index eec68f9fc..853321d3c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -50,6 +50,7 @@ import './CollectionView.scss'; import CollectionMenu from './CollectionMenu'; import { SharingPermissions } from '../../util/SharingManager'; import { PropertiesView } from './collectionFreeForm/PropertiesView'; +import { DocumentView } from '../nodes/DocumentView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -589,6 +590,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus overflow: this.propertiesWidth() < 15 ? "hidden" : undefined }}> <PropertiesView dataDoc={this.dataDoc} Document={this.props.Document} + docView={CollectionView as unknown as DocumentView} width={this._propertiesWidth} height={this.props.PanelHeight()} renderDepth={this.props.renderDepth} diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss index e81b1eeb4..98eae1ac7 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -28,6 +28,18 @@ padding: 8.5px; font-size: 12.5px; font-weight: bold; + + .propertiesView-settings-title { + font-weight: bold; + font-size: 12.5px; + padding-bottom: 7px; + } + + .propertiesView-settings-content { + margin-left: 5px; + padding-bottom: 10px; + } + } .propertiesView-fields { diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx index 27b0dbd9e..5242aa8fd 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -13,6 +13,7 @@ import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentVi import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils"; import { Id } from "../../../../fields/FieldSymbols"; import { Transform } from "../../../util/Transform"; +import { PropertiesButtons } from "../../PropertiesButtons"; interface PropertiesViewProps { @@ -22,7 +23,7 @@ interface PropertiesViewProps { height: number; renderDepth: number; ScreenToLocalTransform: () => Transform; - //docView: DocumentView; + docView: DocumentView; } @observer @@ -125,7 +126,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { Collection </div> <div className="propertiesView-settings"> - Settings + <div className="propertiesView-settings-title"> Settings</div> + <div className="propertiesView-settings-content"> + <PropertiesButtons doc={this.props.Document} /> + </div> </div> <div className="propertiesView-fields"> <div className="propertiesView-fields-title"> Fields</div> |