diff options
86 files changed, 4084 insertions, 1167 deletions
diff --git a/package.json b/package.json index c06b80d79..dac29ddee 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "child_process": "empty" }, "scripts": { - "start-release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts", - "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --transpile-only -- src/server/index.ts", - "oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts", + "start-release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts", + "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts", + "oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts", "debug": "cross-env NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts", "build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production", "test": "mocha -r ts-node/register test/**/*.ts", @@ -253,4 +253,4 @@ "xoauth2": "^1.2.0", "xregexp": "^4.3.0" } -} +}
\ No newline at end of file diff --git a/src/Utils.ts b/src/Utils.ts index a01a94134..0b057dc23 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,8 +1,8 @@ import v4 = require('uuid/v4'); import v5 = require("uuid/v5"); -import { Socket, Room } from 'socket.io'; -import { Message } from './server/Message'; import { ColorState } from 'react-color'; +import { Socket } from 'socket.io'; +import { Message } from './server/Message'; export namespace Utils { export let DRAG_THRESHOLD = 4; diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index f5ee6da3c..6fa8cf909 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -9,7 +9,6 @@ import GestureOverlay from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { runInAction } from 'mobx'; import { ObjectField } from '../fields/ObjectField'; -import { getPlaygroundMode } from '../fields/util'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -452,7 +451,7 @@ export namespace DocServer { } function _UpdateFieldImpl(id: string, diff: any) { - (!getPlaygroundMode()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); + (!DocServer.Control.isReadOnly()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); } let _UpdateField: (id: string, diff: any) => void = errorFunc; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a71450319..3cad02e00 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -153,6 +153,7 @@ export interface DocumentOptions { annotationOn?: Doc; removeDropProperties?: List<string>; // 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; + iconShape?: string; // shapes of the fonticon border linkRelationship?: string; // type of relatinoship a link represents ischecked?: ScriptField; // returns whether a font icon box is checked activeInkPen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) @@ -303,6 +304,10 @@ export namespace Docs { layout: { view: FontIconBox, dataField: defaultDataKey }, options: { _width: 40, _height: 40, borderRounding: "100%" }, }], + // [DocumentType.RECOMMENDATION, { + // layout: { view: RecommendationsBox, dataField: defaultDataKey }, + // options: { _width: 200, _height: 200 }, + // }], [DocumentType.WEBCAM, { layout: { view: DashWebRTCVideo, dataField: defaultDataKey } }], diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 2d95d081e..12e48ae13 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -12,7 +12,7 @@ import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; import { DragManager } from "./DragManager"; import { Scripting } from "./Scripting"; -import { CollectionViewType } from "../views/collections/CollectionView"; +import { CollectionViewType, CollectionView } from "../views/collections/CollectionView"; import { makeTemplate } from "./DropConverter"; import { RichTextField } from "../../fields/RichTextField"; import { PrefetchProxy } from "../../fields/Proxy"; @@ -38,6 +38,8 @@ export class CurrentUserUtils { @observable public static GuestWorkspace: Doc | undefined; @observable public static GuestMobile: Doc | undefined; + @observable public static propertiesWidth: number = 0; + // sets up the default User Templates - slideView, queryView, descriptionView static setupUserTemplateButtons(doc: Doc) { if (doc["template-button-query"] === undefined) { @@ -423,20 +425,21 @@ export class CurrentUserUtils { this.setupActiveMobileMenu(doc); } return [ - { title: "Drag a comparison box", toolTip: "columns", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc }, - { title: "Drag a collection", toolTip: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, - { title: "Drag a web page", toolTip: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc }, - { title: "Drag a cat image", toolTip: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' }, - { title: "Drag a screenshot", toolTip: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' }, - { title: "Drag a webcam", toolTip: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, - { title: "Drag a audio recorder", toolTip: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` }, - { title: "Drag a clickable button", toolTip: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' }, - { title: "Drag a presentation view", toolTip: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc }, - { title: "Drag a search box", toolTip: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.SearchDocument({ _width: 200, title: "an image of a cat" })' }, - { title: "Drag a scripting box", toolTip: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' }, - { title: "Drag an import folder", toolTip: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, - { title: "Drag a mobile view", toolTip: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' }, - { title: "Drag an instance of the device collection", toolTip: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' }, + { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, + { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc }, + { toolTip: "Drag a cat image", title: "Image", icon: "cat", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyImage as Doc }, + { toolTip: "Drag a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc }, + { toolTip: "Drag a screengrabber", title: "Grab", icon: "photo-video", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScreenshot as Doc }, + // { title: "Drag a webcam", title: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, + { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc }, + { toolTip: "Drag a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc }, + + { toolTip: "Drag a presentation view", title: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc }, + { toolTip: "Drag a search box", title: "Query", icon: "search", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptySearch as Doc }, + { toolTip: "Drag a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc }, + // { title: "Drag an import folder", title: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, + { toolTip: "Drag a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, + // { title: "Drag an instance of the device collection", title: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' }, // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, @@ -461,7 +464,7 @@ export class CurrentUserUtils { } const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, + _nativeWidth: 50, _nativeHeight: 50, _width: 50, _height: 50, icon, title, toolTip, @@ -488,6 +491,62 @@ export class CurrentUserUtils { return doc.myItemCreators as Doc; } + static menuBtnDescriptions(): { + title: string, icon: string, click: string, + }[] { + return [ + { title: "Workspace", icon: "desktop", click: 'scriptContext.selectMenu(self, "Workspace")' }, + { title: "Catalog", icon: "file", click: 'scriptContext.selectMenu(self, "Catalog")' }, + { title: "Archive", icon: "archive", click: 'scriptContext.selectMenu(self, "Archive")' }, + { title: "Import", icon: "upload", click: 'scriptContext.selectMenu(self, "Import")' }, + { title: "Sharing", icon: "users", click: 'scriptContext.selectMenu(self, "Sharing")' }, + { title: "Tools", icon: "wrench", click: 'scriptContext.selectMenu(self, "Tools")' }, + { title: "Help", icon: "question-circle", click: 'scriptContext.selectMenu(self, "Help")' }, + { title: "Settings", icon: "cog", click: 'scriptContext.selectMenu(self, "Settings")' }, + { title: "User Doc", icon: "address-card", click: 'scriptContext.selectMenu(self, "UserDoc")' }, + ]; + } + + static setupMenuPanel(doc: Doc) { + if (doc.menuStack === undefined) { + const menuBtns = CurrentUserUtils.menuBtnDescriptions().map(({ title, icon, click }) => + Docs.Create.FontIconDocument({ + icon, + iconShape: "square", + title, + _backgroundColor: "black", + stayInCollection: true, + childDropAction: "same", + _width: 60, + _height: 60, + onClick: ScriptField.MakeScript(click, { scriptContext: "any" }), + })); + const userDoc = menuBtns[menuBtns.length - 1]; + userDoc.target = doc; + userDoc.hidden = ComputedField.MakeFunction("self.target.noviceMode"); + + doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { + title: "menuItemPanel", + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), + _backgroundColor: "black", + _gridGap: 0, + _yMargin: 0, + _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, lockedPosition: true, _chromeStatus: "disabled", + })); + } + // this resets all sidebar buttons to being deactivated + PromiseValue(Cast(doc.menuStack, Doc)).then(stack => { + stack && PromiseValue(stack.data).then(btns => { + DocListCastAsync(btns).then(bts => bts?.forEach(btn => { + btn.color = "white"; + btn._backgroundColor = ""; + })); + }) + }); + return doc.menuStack as Doc; + } + + // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu static setupActiveMobileMenu(doc: Doc) { if (doc.activeMobileMenu === undefined) { @@ -586,10 +645,6 @@ export class CurrentUserUtils { return Cast(userDoc.thumbDoc, Doc); } - static setupLibrary(userDoc: Doc) { - return CurrentUserUtils.setupWorkspaces(userDoc); - } - // 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 async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) { @@ -597,6 +652,8 @@ export class CurrentUserUtils { const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc); const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); + doc["tabs-button-tools"] = undefined; + if (doc.myCreators === undefined) { doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], { title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0, @@ -611,88 +668,82 @@ export class CurrentUserUtils { doc.myColorPicker = new PrefetchProxy(color); } - if (doc["tabs-button-tools"] === undefined) { + if (doc["sidebar-tools"] === undefined) { const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true + title: "sidebar-tools", _width: 500, lockedPosition: true, _chromeStatus: "disabled", hideFilterView: true, forceActive: true })) as any as Doc; - doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({ - _width: 35, _height: 25, title: "Tools", _fontSize: "10pt", - letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: toolsStack, - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: toolsStack, - removeDropProperties: new List<string>(["lockedPosition"]), - stayInCollection: true, - targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), - })); + + doc["sidebar-tools"] = toolsStack; } - (doc["tabs-button-tools"] as Doc).sourcePanel; // prefetch sourcePanel - return doc["tabs-button-tools"] as Doc; } static setupWorkspaces(doc: Doc) { // setup workspaces library item + doc.myWorkspaces === undefined; if (doc.myWorkspaces === undefined) { doc.myWorkspaces = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, + title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, treeViewOpen: true, })); } - const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`); - (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]); - (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]); + if (doc["sidebar-workspaces"] === undefined) { + const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`); + (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]); + (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]); - return doc.myWorkspaces as Doc; + const workspaces = doc.myWorkspaces as Doc; + + doc["sidebar-workspaces"] = new PrefetchProxy(Docs.Create.TreeDocument([workspaces], { + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; + } } + static setupCatalog(doc: Doc) { + doc.myCatalog === undefined; if (doc.myCatalog === undefined) { doc.myCatalog = new PrefetchProxy(Docs.Create.SchemaDocument([], [], { title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, - childDropAction: "alias", targetDropAction: "same", stayInCollection: true, + childDropAction: "alias", targetDropAction: "same", stayInCollection: true, treeViewOpen: true, })); } - return doc.myCatalog as Doc; + + if (doc["sidebar-catalog"] === undefined) { + const catalog = doc.myCatalog as Doc; + + doc["sidebar-catalog"] = new PrefetchProxy(Docs.Create.TreeDocument([catalog], { + title: "sidebar-catalog", + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; + } } static setupRecentlyClosed(doc: Doc) { // setup Recently Closed library item + doc.myRecentlyClosed === undefined; if (doc.myRecentlyClosed === undefined) { doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, stayInCollection: true, + title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, treeViewOpen: true, stayInCollection: true, })); } // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready PromiseValue(Cast(doc.myRecentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast)); - const clearAll = ScriptField.MakeScript(`self.data = new List([])`); - (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); + if (doc["sidebar-recentlyClosed"] === undefined) { + const clearAll = ScriptField.MakeScript(`self.data = new List([])`); + (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); - return doc.myRecentlyClosed as Doc; - } - // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views - static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) { - const workspaces = CurrentUserUtils.setupWorkspaces(doc); - const documents = CurrentUserUtils.setupCatalog(doc); - const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc); - - if (doc["tabs-button-library"] === undefined) { - const libraryStack = new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { - title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, + const recentlyClosed = doc.myRecentlyClosed as Doc; + + doc["sidebar-recentlyClosed"] = new PrefetchProxy(Docs.Create.TreeDocument([recentlyClosed], { + title: "sidebar-recentlyClosed", + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" })) as any as Doc; - doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Library", _fontSize: "10pt", targetDropAction: "same", - letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: libraryStack, - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: libraryStack, - removeDropProperties: new List<string>(["lockedPosition"]), - stayInCollection: true, - targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") - })); } - return doc["tabs-button-library"] as Doc; } // setup the Search button which will display the search panel. @@ -708,45 +759,44 @@ export class CurrentUserUtils { onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") })); } - return doc["tabs-button-search"] as Doc; + } + static setupUserDoc(doc: Doc) { + if (doc["sidebar-userDoc"] === undefined) { + doc.treeViewOpen = true; + doc.treeViewExpandedView = "fields"; + doc["sidebar-userDoc"] = new PrefetchProxy(Docs.Create.TreeDocument([doc], { + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "sidebar-userDoc", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; + } } static setupSidebarContainer(doc: Doc) { - if (doc["tabs-panelContainer"] === undefined) { + if (doc["sidebar"] === undefined) { const sidebarContainer = new Doc(); sidebarContainer._chromeStatus = "disabled"; sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); - doc["tabs-panelContainer"] = new PrefetchProxy(sidebarContainer); + doc["sidebar"] = new PrefetchProxy(sidebarContainer); } - return doc["tabs-panelContainer"] as Doc; + return doc["sidebar"] as Doc; } // setup the list of sidebar mode buttons which determine what is displayed in the sidebar static async setupSidebarButtons(doc: Doc) { const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc); const toolsBtn = await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); - const libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer); const searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer); if (doc["search-panel"] === undefined) { doc["search-panel"] = new PrefetchProxy(Docs.Create.SearchDocument({ _width: 500, _height: 400, backgroundColor: "dimGray", ignoreClick: true, childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack", })) as any as Doc; } - // Finally, setup the list of buttons to display in the sidebar - if (doc["tabs-buttons"] === undefined) { - doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([libraryBtn, searchBtn, toolsBtn], { - _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", _columnsHideIfEmpty: true, ignoreClick: true, _chromeStatus: "view-mode", - title: "sidebar btn row stack", backgroundColor: "dimGray", - })); - (toolsBtn.onClick as ScriptField).script.run({ this: toolsBtn }); - } - - - - // new PrefetchProxy(Docs.Create.StackingDocument([libraryBtn, searchBtn, toolsBtn], { - // _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", _columnsHideIfEmpty: true, ignoreClick: true, _chromeStatus: "view-mode", - // title: "sidebar btn row stack", backgroundColor: "dimGray", - // })); + await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); + CurrentUserUtils.setupWorkspaces(doc); + CurrentUserUtils.setupCatalog(doc); + CurrentUserUtils.setupRecentlyClosed(doc); + CurrentUserUtils.setupUserDoc(doc); } static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { @@ -756,7 +806,7 @@ export class CurrentUserUtils { })) as any as Doc static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 + ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40 })) as any as Doc /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window @@ -860,6 +910,8 @@ export class CurrentUserUtils { doc.activeDash = StrCast(doc.activeDash, "0"); doc.fontSize = StrCast(doc.fontSize, "12pt"); doc.fontFamily = StrCast(doc.fontFamily, "Arial"); + doc.fontColor = StrCast(doc.fontColor, "black"); + doc.fontHighlight = StrCast(doc.fontHighlight, ""); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); @@ -867,6 +919,7 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile + this.setupMenuPanel(doc); this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons this.setupDefaultPresentation(doc); // presentation that's initially triggered diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 72fba5c1b..5215ea35f 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -20,6 +20,9 @@ import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); +/** + * Interface for options for the react-select component + */ export interface UserOptions { label: string; value: string; @@ -30,15 +33,13 @@ export default class GroupManager extends React.Component<{}> { static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. - @observable private dialogueBoxOpacity: number = 1; // opacity of the dialogue box div of the MainViewModal. - @observable private overlayOpacity: number = 0.4; // opacity of the overlay div of the MainViewModal. @observable private users: string[] = []; // list of users populated from the database. @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown. @observable currentGroup: Opt<Doc>; // the currently selected group. @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. - private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); - private currentUserGroups: string[] = []; + private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button + private currentUserGroups: string[] = []; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; @@ -49,6 +50,9 @@ export default class GroupManager extends React.Component<{}> { GroupManager.Instance = this; } + /** + * Populates the list of users and groups. + */ componentDidMount() { this.populateUsers(); this.populateGroups(); @@ -62,8 +66,6 @@ export default class GroupManager extends React.Component<{}> { const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; const evaluating = raw.map(async user => { - // const isCandidate = user.email !== Doc.CurrentUserEmail; - // if (isCandidate) { const userDocument = await DocServer.GetRefField(user.userDocumentId); if (userDocument instanceof Doc) { const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc); @@ -73,11 +75,13 @@ export default class GroupManager extends React.Component<{}> { } }); } - // } }); return Promise.all(evaluating); } + /** + * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts + */ populateGroups = () => { DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { groups?.forEach(group => { @@ -101,7 +105,7 @@ export default class GroupManager extends React.Component<{}> { */ @action open = () => { - SelectionManager.DeselectAll(); + // SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); this.populateGroups(); @@ -145,25 +149,8 @@ export default class GroupManager extends React.Component<{}> { } /** - * @returns a readonly copy of a single group document + * Returns an array of the list of members of a given group. */ - getGroupCopy(groupName: string): Doc | undefined { - const groupDoc = this.getGroup(groupName); - if (groupDoc) { - const { members, owners } = groupDoc; - return Doc.assign(new Doc, { groupName, members: StrCast(members), owners: StrCast(owners) }); - } - return undefined; - } - /** - * @returns a readonly copy of the list of group documents - */ - getAllGroupsCopy(): Doc[] { - return this.getAllGroups().map(({ groupName, owners, members }) => - Doc.assign(new Doc, { groupName: (StrCast(groupName)), owners: (StrCast(owners)), members: (StrCast(members)) }) - ); - } - getGroupMembers(group: string | Doc): string[] { if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[]; else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; @@ -316,6 +303,9 @@ export default class GroupManager extends React.Component<{}> { } + /** + * @returns the MainViewModal which allows the user to create groups. + */ private get groupCreationModal() { const contents = ( <div className="group-create"> @@ -415,7 +405,7 @@ export default class GroupManager extends React.Component<{}> { <div className="sort-groups" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} + Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} </div> <div className="group-body"> {groups.map(group => diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss index c609c5c7b..2eb164988 100644 --- a/src/client/util/GroupMemberView.scss +++ b/src/client/util/GroupMemberView.scss @@ -41,9 +41,10 @@ margin-top: -5; height: 20; text-overflow: ellipsis; + background: none; &:hover { - text-overflow: visible; + text-overflow: unset; overflow-x: auto; } } @@ -72,7 +73,7 @@ .editing-contents { overflow-y: auto; - height: 65%; + height: 62%; width: 100%; color: black; margin-top: -15px; diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index f20670c4e..531ef988a 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -29,13 +29,17 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : []; + const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group); + return (!this.props.group ? null : <div className="editing-interface"> <div className="editing-header"> <input className="group-title" + style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }} value={StrCast(this.props.group.groupName)} onChange={e => this.props.group.groupName = e.currentTarget.value} + disabled={!hasEditAccess} > </input> <div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}> @@ -65,12 +69,15 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp null} <div className="sort-emails" + style={{ paddingTop: hasEditAccess ? 0 : 35 }} onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}> Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */} </div> </div> <hr /> - <div className="editing-contents"> + <div className="editing-contents" + style={{ height: hasEditAccess ? "62%" : "85%" }} + > {members.map(member => ( <div className="editing-row" @@ -79,7 +86,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp <div className="user-email"> {member} </div> - {GroupManager.Instance.hasEditAccess(this.props.group) ? + {hasEditAccess ? <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> <FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} /> </div> diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 20d881961..05ba00331 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -12,6 +12,7 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); + @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { @@ -32,6 +33,7 @@ export namespace SelectionManager { } @action DeselectDoc(docView: DocumentView): void { + if (manager.SelectedDocuments.get(docView)) { manager.SelectedDocuments.delete(docView); docView.props.whenActiveChanged(false); @@ -40,6 +42,7 @@ export namespace SelectionManager { } @action DeselectAll(): void { + Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments.clear(); Doc.UserDoc().activeSelection = new List<Doc>([]); diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index c1627e69f..6923fe879 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -7,7 +7,7 @@ height: 300px; button { - background: $lighter-alt-accent; + background: #315a96; outline: none; border-radius: 5px; border: 0px; @@ -29,8 +29,12 @@ button { width: 100%; align-self: center; - background: $darker-alt-accent; + background: #252b33; margin-top: 4px; + + &:hover { + background: $main-accent; + } } .delete-button { @@ -102,11 +106,12 @@ } h1 { - color: $dark-color; + color: #121721; text-transform: uppercase; letter-spacing: 2px; - font-size: 120%; + font-size: 19; margin-top: 0; + font-weight: bold; } .container { @@ -151,7 +156,7 @@ .settings-interface button { width: 100%; font-size: 30px; - background: #b2cef8; + background: #315a96; } .settings-interface .settings-heading { diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index fbdceb43e..6276fae96 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -14,7 +14,6 @@ import { Doc } from "../../fields/Doc"; import GroupManager from "./GroupManager"; import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager"; import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; -import { togglePlaygroundMode } from "../../fields/util"; import { DocServer } from "../DocServer"; library.add(fa.faTimes); @@ -101,7 +100,6 @@ export default class SettingsManager extends React.Component<{}> { @action togglePlaygroundMode = () => { - //togglePlaygroundMode(); this.playgroundMode = !this.playgroundMode; if (this.playgroundMode) DocServer.Control.makeReadOnly(); else DocServer.Control.makeEditable(); @@ -138,7 +136,7 @@ export default class SettingsManager extends React.Component<{}> { {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined} {this.successText ? <div className="success-text">{this.successText}</div> : undefined} <button onClick={this.dispatchRequest}>submit</button> - <a href="/forgotPassword">forgot password?</a> + <a style={{ marginLeft: 65, marginTop: -20 }} href="/forgotPassword">forgot password?</a> </div> : undefined} diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 130785672..8da80ef52 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -1,6 +1,6 @@ .sharing-interface { width: 600px; - height: 360px; + // height: 360px; .overlay { transform: translate(-20px, -20px); @@ -23,33 +23,51 @@ z-index: 999; } - .share-setup { - display: flex; - margin-bottom: 20px; - align-items: center; - height: 36; + .share-container { + .share-setup { + display: flex; + margin-bottom: 20px; + align-items: center; + height: 36; - .user-search { - width: 90%; + .user-search { + width: 90%; - input { - height: 30; + input { + height: 30; + } + } + + .permissions-select { + z-index: 1; + margin-left: -100; + border: none; + outline: none; + text-align: justify; // for Edge + text-align-last: end; } - } - .permissions-select { - z-index: 1; - margin-left: -100; - border: none; - outline: none; - text-align: justify; // for Edge - text-align-last: end; + .share-button { + height: 105%; + margin-left: 2%; + background-color: black; + } } - .share-button { - height: 105%; - margin-left: 2%; - background-color: #979797; + .sort-checkboxes { + float: left; + margin-top: -17px; + margin-bottom: 10px; + font-size: 10px; + + input { + height: 10px; + } + + label { + font-weight: normal; + font-style: italic; + } } } @@ -92,10 +110,8 @@ height: 250px; margin: 0 2; - .none { font-style: italic; - } } } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 0d8b33fbe..7a0fe4140 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,7 +1,7 @@ import { observable, runInAction, action } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, DocListCastAsync } from "../../fields/Doc"; +import { Doc, Opt, DocListCastAsync, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; @@ -19,7 +19,7 @@ import GroupMemberView from "./GroupMemberView"; import Select from "react-select"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { List } from "../../fields/List"; -import { distributeAcls, SharingPermissions } from "../../fields/util"; +import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; export interface User { @@ -27,7 +27,10 @@ export interface User { userDocumentId: string; } -interface GroupOptions { +/** + * Interface for grouped options for the react-select component. + */ +interface GroupedOptions { label: string; options: UserOptions[]; } @@ -36,9 +39,13 @@ interface GroupOptions { // const PublicKey = "publicLinkPermissions"; // const DefaultColor = "black"; -const groupType = "!groupType/"; +// used to differentiate between individuals and groups when sharing const indType = "!indType/"; +const groupType = "!groupType/"; +/** + * A user who also has a notificationDoc. + */ interface ValidatedUser { user: User; notificationDoc: Doc; @@ -49,41 +56,43 @@ const storage = "data"; @observer export default class SharingManager extends React.Component<{}> { public static Instance: SharingManager; - @observable private isOpen = false; - @observable private users: ValidatedUser[] = []; - @observable private targetDoc: Doc | undefined; - @observable private targetDocView: DocumentView | undefined; + @observable private isOpen = false; // whether the SharingManager modal is open or not + @observable private users: ValidatedUser[] = []; // the list of users with notificationDocs + @observable private targetDoc: Doc | undefined; // the document being shared + @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared // @observable private copied = false; - @observable private dialogueBoxOpacity = 1; - @observable private overlayOpacity = 0.4; - @observable private selectedUsers: UserOptions[] | null = null; - @observable private permissions: SharingPermissions = SharingPermissions.Edit; - @observable private individualSort: "ascending" | "descending" | "none" = "none"; - @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); - - + @observable private dialogueBoxOpacity = 1; // for the modal + @observable private overlayOpacity = 0.4; // for the modal + @observable private selectedUsers: UserOptions[] | null = null; // users (individuals/groups) selected to share with + @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users + @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals + @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups + private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + // if both showUserOptions and showGroupOptions are false then both are displayed + @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) + @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; // } public open = (target: DocumentView) => { - SelectionManager.DeselectAll(); - this.populateUsers().then(action(() => { + runInAction(() => this.users = []); + // SelectionManager.DeselectAll(); + this.populateUsers(); + runInAction(() => { this.targetDocView = target; this.targetDoc = target.props.Document; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = true; this.permissions = SharingPermissions.Edit; - })); + }); } public close = action(() => { this.isOpen = false; - this.users = []; - this.selectedUsers = null; + this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component) setTimeout(action(() => { // this.copied = false; @@ -97,7 +106,18 @@ export default class SharingManager extends React.Component<{}> { SharingManager.Instance = this; } + /** + * Populates the list of users. + */ + componentDidMount() { + this.populateUsers(); + } + + /** + * Populates the list of validated users (this.users) by adding registered users which have a rightSidebarCollection. + */ populateUsers = async () => { + runInAction(() => this.users = []); const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; const evaluating = raw.map(async user => { @@ -117,58 +137,80 @@ export default class SharingManager extends React.Component<{}> { return Promise.all(evaluating); } - setInternalGroupSharing = (group: Doc, permission: string) => { + /** + * Sets the permission on the target for the group. + * @param group + * @param permission + */ + setInternalGroupSharing = (group: Doc, permission: string, targetDoc?: Doc) => { const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - const target = this.targetDoc!; + const target = targetDoc || this.targetDoc!; const ACL = `ACL-${StrCast(group.groupName)}`; - // fix this - not needed (here and setinternalsharing and removegroup) - // target[ACL] = permission; - // Doc.GetProto(target)[ACL] = permission; - distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); + // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]); users.forEach(({ notificationDoc }) => { DocListCastAsync(notificationDoc[storage]).then(resolved => { - if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); + if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added + else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists }); }); } + /** + * Shares the documents shared with a group with a new user who has been added to that group. + * @param group + * @param emailId + */ shareWithAddedMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { DocListCastAsync(group.docsShared).then(docsShared => { docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); // add the doc if it isn't already in the list }); }); } } + shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => { + const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); + if (user) this.setInternalSharing(user, permission, target); + else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, target); + } + + /** + * Removes the documents shared with a user through a group when the user is removed from the group. + * @param group + * @param emailId + */ removeMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { DocListCastAsync(group.docsShared).then(docsShared => { docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); // remove the doc only if it is in the list }); }); } } + /** + * Removes a group's permissions from documents that have been shared with it. + * @param group + */ removeGroup = (group: Doc) => { if (group.docsShared) { DocListCastAsync(group.docsShared).then(resolved => { resolved?.forEach(doc => { const ACL = `ACL-${StrCast(group.groupName)}`; - // doc[ACL] = doc[DataSym][ACL] = "Not Shared"; distributeAcls(ACL, SharingPermissions.None, doc); @@ -182,14 +224,13 @@ export default class SharingManager extends React.Component<{}> { } } - // @action - setInternalSharing = (recipient: ValidatedUser, permission: string) => { + setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { const { user, notificationDoc } = recipient; - const target = this.targetDoc!; + const target = targetDoc || this.targetDoc!; const key = user.email.replace('.', '_'); const ACL = `ACL-${key}`; - distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); if (permission !== SharingPermissions.None) { DocListCastAsync(notificationDoc[storage]).then(resolved => { @@ -291,7 +332,7 @@ export default class SharingManager extends React.Component<{}> { const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect(); TaskCompletionBox.popupX = left - 1.5 * width; - TaskCompletionBox.popupY = top - height; + TaskCompletionBox.popupY = top - 1.5 * height; TaskCompletionBox.textDisplayed = "Document shared!"; TaskCompletionBox.taskCompleted = true; setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); @@ -315,45 +356,67 @@ export default class SharingManager extends React.Component<{}> { private get sharingInterface() { const groupList = GroupManager.Instance?.getAllGroups() || []; - const sortedUsers = this.users.sort(this.sortUsers) + const sortedUsers = this.users.slice().sort(this.sortUsers) .map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList.sort(this.sortGroups) + const sortedGroups = groupList.slice().sort(this.sortGroups) .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); - const options: GroupOptions[] = GroupManager.Instance ? - [ - { + const options: GroupedOptions[] = []; + + if (GroupManager.Instance) { + if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { + options.push({ label: 'Individuals', options: sortedUsers }, - { + { + label: 'Groups', + options: sortedGroups + }); + } + else if (this.showUserOptions) { + options.push({ + label: 'Individuals', + options: sortedUsers + }); + } + else { + options.push({ label: 'Groups', options: sortedGroups - } - ] - : []; + }); + } + } const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users; const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList; + const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate; + const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { const userKey = user.email.replace('.', '_'); - const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None); + const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]); - return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : ( + return !permissions || user.email === this.targetDoc?.author ? null : ( <div key={userKey} className={"container"} > <span className={"padding"}>{user.email}</span> <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} - > - {this.sharingOptions} - </select> + {effectiveAcl === AclAdmin ? ( + <select + className={"permissions-dropdown"} + value={permissions} + onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} + > + {this.sharingOptions} + </select> + ) : ( + <div className={"permissions-dropdown"}> + {permissions} + </div> + )} </div> </div> ); @@ -365,20 +428,34 @@ export default class SharingManager extends React.Component<{}> { key={"owner"} className={"container"} > - <span className={"padding"}>{this.targetDoc?.author}</span> + <span className={"padding"}>{this.targetDoc?.author === Doc.CurrentUserEmail ? "Me" : this.targetDoc?.author}</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> Owner </div> </div> </div> - ) + ), + this.targetDoc?.author !== Doc.CurrentUserEmail ? + ( + <div + key={"me"} + className={"container"} + > + <span className={"padding"}>Me</span> + <div className="edit-actions"> + <div className={"permissions-dropdown"}> + {this.targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]} + </div> + </div> + </div> + ) : null ); const groupListContents = groups.map(group => { - const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`], SharingPermissions.None); + const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]); - return permissions === SharingPermissions.None ? null : ( + return !permissions ? null : ( <div key={StrCast(group.groupName)} className={"container"} @@ -400,7 +477,6 @@ export default class SharingManager extends React.Component<{}> { ); }); - const displayUserList = !userListContents?.every(user => user === null); const displayGroupList = !groupListContents?.every(group => group === null); return ( @@ -446,8 +522,7 @@ export default class SharingManager extends React.Component<{}> { <div className={"close-button"} onClick={this.close}> <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> - {this.targetDoc?.author !== Doc.CurrentUserEmail ? null - : + {<div className="share-container"> <div className="share-setup"> <Select className={"user-search"} @@ -457,6 +532,11 @@ export default class SharingManager extends React.Component<{}> { options={options} onChange={this.handleUsersChange} value={this.selectedUsers} + styles={{ + indicatorSeparator: () => ({ + visibility: "hidden" + }) + }} /> <select className="permissions-select" onChange={this.handlePermissionsChange}> {this.sharingOptions} @@ -465,6 +545,11 @@ export default class SharingManager extends React.Component<{}> { Share </button> </div> + <div className="sort-checkboxes"> + <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> + <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> + </div> + </div> } <div className="main-container"> <div className={"individual-container"}> @@ -473,17 +558,8 @@ export default class SharingManager extends React.Component<{}> { onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */} </div> - <div className={"users-list"} style={{ display: !displayUserList ? "flex" : "block" }}>{/*200*/} - { - !displayUserList ? - <div - className={"none"} - > - There are no users this document has been shared with. - </div> - : - userListContents - } + <div className={"users-list"} style={{ display: "block" }}>{/*200*/} + {userListContents} </div> </div> <div className={"group-container"}> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 81432968d..7e233ec04 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -80,7 +80,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select render() { if ("event" in this.props) { return ( - <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onClick={this.handleEvent}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onPointerDown={this.handleEvent}> {this.props.icon ? ( <span className="icon-background"> <FontAwesomeIcon icon={this.props.icon} size="sm" /> @@ -95,7 +95,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select const where = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "flex-start" : this._overPosY > window.innerHeight * 2 / 3 ? "flex-end" : "center"; const marginTop = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "20px" : this._overPosY > window.innerHeight * 2 / 3 ? "-20px" : ""; const submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px", marginTop }}> + <div className="contextMenu-subMenu-cont" style={{ marginLeft: "90%", left: "0px", marginTop }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; if (!("noexpand" in this.props)) { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 177c1b5ee..451bb8065 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, getPlaygroundMode, SharingPermissions } from '../../fields/util'; +import { GetEffectiveAcl, SharingPermissions } from '../../fields/util'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -150,16 +150,29 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const effectiveAcl = GetEffectiveAcl(this.dataDoc); if (added.length) { - if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { + if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { return false; - } else if (this.dataDoc[AclSym] === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); - } else { - added.map(doc => doc.context = this.props.Document); - targetDataDoc[this.annotationKey] = new List<Doc>([...docList, ...added]); - targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); - targetDataDoc["lastModified"] = new DateField(new Date(Date.now())); - + } + else { + // if (this.props.Document[AclSym]) { + // added.forEach(d => { + // const dataDoc = d[DataSym]; + // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; + // for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + // dataDoc[key] = d[key] = this.AclMap.get(value); + // } + // }); + // } + if (effectiveAcl === AclAddonly) { + added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); + } + else { + added.map(doc => doc.context = this.props.Document); + (targetDataDoc[this.annotationKey] as List<Doc>).push(...added); + targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); + targetDataDoc["lastModified"] = new DateField(new Date(Date.now())); + + } } } return true; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 6b85616c2..c9f380737 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -278,10 +278,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-button"> <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> - {DocumentLinksButton.StartLink ? <div className="documentButtonBar-button"> - <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> - </div> : null} <div className="documentButtonBar-button"> + <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> + </div> + {/* <div className="documentButtonBar-button"> {this.templateButton} </div> <div className="documentButtonBar-button"> @@ -289,16 +289,16 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </div> <div className="documentButtonBar-button"> {this.contextButton} - </div> + </div> */} <div className="documentButtonBar-button"> {this.pinButton} </div> - <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> + {/* <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} </div> <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> {this.considerGoogleDocsPull} - </div> + </div> */} </div>; } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 51325ae1b..f16cb273b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,9 +1,9 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause } from '@fortawesome/free-solid-svg-icons'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction } from "mobx"; +import { action, computed, observable, reaction, runInAction, get } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, Field, WidthSym, HeightSym } from "../../fields/Doc"; +import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc"; import { Document } from '../../fields/documentSchemas'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, StrCast, NumCast } from "../../fields/Types"; @@ -23,6 +23,10 @@ import { SnappingManager } from '../util/SnappingManager'; import { HtmlField } from '../../fields/HtmlField'; import { InkField } from "../../fields/InkField"; import { Tooltip } from '@material-ui/core'; +import { GetEffectiveAcl } from '../../fields/util'; +import { DocumentIcon } from './nodes/DocumentIcon'; +import { render } from 'react-dom'; +import { createLessThan } from 'typescript'; library.add(faCaretUp); library.add(faObjectGroup); @@ -44,6 +48,7 @@ library.add(faAngleDoubleRight); library.add(faAngleLeft); library.add(faAngleRight); library.add(faPause); +library.add(faExternalLinkAlt); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -152,8 +157,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (e.button === 0 && !e.altKey && !e.ctrlKey) { let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0]; while (child.children.length) { - const next = Array.from(child.children).find(c => typeof (c.className) !== "string"); - if (typeof (next?.className) === "string" && next?.className.includes("documentView-node")) break; + const next = Array.from(child.children).find(c => typeof (c.className) === "string"); + if (next?.className.includes("documentView-node")) break; if (next) child = next; else break; } @@ -194,8 +199,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> SelectionManager.DeselectAll(); selected.map(dv => { - recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); - dv.props.removeDocument?.(dv.props.Document); + const effectiveAcl = GetEffectiveAcl(dv.props.Document); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete + recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); + dv.props.removeDocument?.(dv.props.Document); + } }); } } @@ -527,12 +535,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const ink = Cast(doc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number, Y: number }[] = []; - for (var i = 0; i < ink.length; i++) { + ink.forEach(i => { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = (doc.x - this._inkDocs[index].x) + (ink[i].X * doc._width) / this._inkDocs[index].width; - const newY = (doc.y - this._inkDocs[index].y) + (ink[i].Y * doc._height) / this._inkDocs[index].height; + const newX = ((doc.x || 0) - this._inkDocs[index].x) + (i.X * (doc._width || 0)) / this._inkDocs[index].width; + const newY = ((doc.y || 0) - this._inkDocs[index].y) + (i.Y * (doc._height || 0)) / this._inkDocs[index].height; newPoints.push({ X: newX, Y: newY }); - } + }); doc.data = new InkField(newPoints); } @@ -580,17 +588,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } + const canDelete = SelectionManager.SelectedDocuments().map(docView => GetEffectiveAcl(docView.props.ContainingCollectionDoc)).some(permission => permission === AclAdmin || permission === AclEdit); const minimal = bounds.r - bounds.x < 100 ? true : false; const maximizeIcon = minimal ? ( <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"> <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> - </div></Tooltip>) : ( - <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> - <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> - {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} - <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> - </div></Tooltip>); + </div></Tooltip>) : canDelete ? ( + <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> + <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> + {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} + <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> + </div></Tooltip>) : (null); const titleArea = this._edtingTitle ? <> @@ -659,7 +668,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {"_"} </div></Tooltip>} <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}> - {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} + {SelectionManager.SelectedDocuments().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} </div></Tooltip> <div id="documentDecorations-rotation" className="documentDecorations-rotation" onPointerDown={this.onRotateDown}> ⟲ </div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 0435e70c4..a98ef97ea 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -221,11 +221,11 @@ export class EditableView extends React.Component<EditableProps> { {this.renderEditor()} </div> : this.renderEditor(); } else { - this.props.autosuggestProps?.resetValue(); + setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); return (this.props.contents instanceof ObjectField ? (null) : <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} - style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} + style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick} placeholder={this.props.placeholder}> {this.props.highlight === undefined || this.props.positions === undefined || this.props.bing === undefined ? <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> : this.returnHighlights()} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 5892e8346..8e3f72cee 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -178,7 +178,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume if (cm) { !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); cm.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); - cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); + //cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); } }} ><defs> diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index e1ddbc533..246e0cdfb 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -102,6 +102,44 @@ user-select: none; } +.mainView-propertiesDragger { + background-color: rgb(140, 139, 139); + height: 55px; + width: 17px; + position: absolute; + top: 55%; + border: 1px black solid; + border-radius: 0; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + border-right: unset; + z-index: 2; + + .mainView-propertiesDragger-icon { + width: 10px; + height: 10px; + float: left; + margin-left: 5.5px; + padding-top: 19px; + } + + &:hover { + cursor: grab; + } +} + +.mainiView-propertiesView { + display: flex; + flex-direction: column; + height: 100%; + position: absolute; + right: 0; + top: 0; + border-left: solid 1px; + z-index: 100000; + cursor: auto; +} + .mainView-flyoutContainer { display: flex; flex-direction: column; @@ -114,6 +152,74 @@ } } +.mainView-menuPanel { + + width: 60px; + background-color: black; + height: 100%; + //overflow-y: scroll; + //overflow-x: hidden; + + + .mainView-menuPanel-button { + padding: 7px; + padding-left: 7px; + width: 100%; + + .mainView-menuPanel-button-wrap { + width: 45px; + /* padding: 5px; */ + touch-action: none; + background: black; + transform-origin: top left; + /* margin-bottom: 5px; */ + margin-top: 5px; + margin-right: 25px; + border-radius: 8px; + + &:hover { + background: rgb(61, 61, 61); + cursor: pointer; + } + } + } + + .mainView-menuPanel-button-label { + color: white; + margin-left: px; + margin-right: 4px; + border-radius: 8px; + width: 42px; + position: relative; + text-align: center; + font-size: 8px; + margin-top: 1px; + letter-spacing: normal; + padding: 3px; + background-color: inherit; + } + + .mainView-menuPanel-button-icon { + width: auto; + height: 35px; + padding: 5px; + } + + svg { + width: 95% !important; + height: 95%; + } +} + +.mainView-searchPanel { + width: 100%; + height: 32px; + background-color: black; + color: white; + text-align: center; + vertical-align: middle; +} + .mainView-mainDiv { width: 100%; height: 100%; @@ -162,26 +268,44 @@ display: flex; flex-direction: column; z-index: 2; + + .mainView-libraryFlyout-close { + right: 6; + top: 5; + position: absolute; + margin-right: 6px; + z-index: 10; + margin-bottom: 10; + } } .mainView-expandFlyoutButton { position: absolute; - top: 100px; - right: 30px; + top: 120px; + right: 55px; cursor: pointer; } .mainView-libraryHandle { - width: 20px; + width: 28px; left: calc(100% - 10px); - height: 40px; + height: 55px; top: 50%; border: 1px solid black; - border-radius: 5px; + border-radius: 8px; position: absolute; z-index: 2; touch-action: none; - cursor: ew-resize; + cursor: grab; + + .mainView-libraryHandle-icon { + width: 10px; + height: 10px; + float: right; + margin-right: 3px; + padding-top: 19px; + } + } .mainView-workspace { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8995aa78b..03f624038 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,7 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faHireAHelper, faBuffer } from '@fortawesome/free-brands-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -12,52 +11,56 @@ import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; +import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; -import { CurrentUserUtils } from '../util/CurrentUserUtils'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils, returnEmptyFilter } from '../../Utils'; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { DocumentManager } from '../util/DocumentManager'; +import GroupManager from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; -import RichTextMenu from './nodes/formattedText/RichTextMenu'; import { Scripting } from '../util/Scripting'; +import { SelectionManager } from '../util/SelectionManager'; import SettingsManager from '../util/SettingsManager'; -import GroupManager from '../util/GroupManager'; import SharingManager from '../util/SharingManager'; +import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; +import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; +import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; +import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; import { CollectionLinearView } from './collections/CollectionLinearView'; +import CollectionMenu from './collections/CollectionMenu'; import { CollectionView, CollectionViewType } from './collections/CollectionView'; import { ContextMenu } from './ContextMenu'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import GestureOverlay from './GestureOverlay'; +import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import KeyManager from './GlobalKeyHandler'; +import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; import { MainViewNotifs } from './MainViewNotifs'; import { AudioBox } from './nodes/AudioBox'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import RichTextMenu from './nodes/formattedText/RichTextMenu'; +import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; +import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; +import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; -import { ScriptField } from '../../fields/ScriptField'; -import { TimelineMenu } from './animationtimeline/TimelineMenu'; -import { SnappingManager } from '../util/SnappingManager'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { DocumentManager } from '../util/DocumentManager'; -import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { LinkMenu } from './linking/LinkMenu'; -import { LinkDocPreview } from './nodes/LinkDocPreview'; +import { undoBatch } from '../util/UndoManager'; import { SearchBox } from './search/SearchBox'; -import { TaskCompletionBox } from './nodes/TaskCompletedBox'; -import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; -import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; -import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; -import CollectionMenu from './collections/CollectionMenu'; @observer export class MainView extends React.Component { @@ -70,8 +73,8 @@ export class MainView extends React.Component { @observable private _panelWidth: number = 0; @observable private _panelHeight: number = 0; - @observable private _flyoutTranslate: boolean = true; - @observable public flyoutWidth: number = 250; + @observable private _flyoutTranslate: boolean = false; + @observable public flyoutWidth: number = 0; private get darkScheme() { return BoolCast(Cast(this.userDoc?.activeWorkspace, Doc, null)?.darkScheme); } @computed private get userDoc() { return Doc.UserDoc(); } @@ -81,7 +84,26 @@ export class MainView extends React.Component { @computed public get searchDoc() { return Cast(this.userDoc["search-panel"], Doc) as Doc; } + @observable public sidebarContent: any = this.userDoc?.["sidebar"]; + @observable public panelContent: string = "none"; + @observable public showProperties: boolean = false; public isPointerDown = false; + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + + propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth)); + + @computed get propertiesIcon() { + if (this.propertiesWidth() < 10) { + return "chevron-left"; + } else { + return "chevron-right"; + } + } + @observable propertiesDownX: number | undefined; componentDidMount() { DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's @@ -121,6 +143,9 @@ export class MainView extends React.Component { MainView.Instance = this; this._urlState = HistoryUtil.parseUrl(window.location) || {} as any; // causes errors to be generated when modifying an observable outside of an action + + CurrentUserUtils.propertiesWidth = 0; + configure({ enforceActions: "observed" }); if (window.location.pathname !== "/home") { const pathname = window.location.pathname.substr(1).split("/"); @@ -129,7 +154,7 @@ export class MainView extends React.Component { if (type === "doc") { CurrentUserUtils.MainDocId = pathname[1]; if (!this.userDoc) { - runInAction(() => this.flyoutWidth = 0); + runInAction(() => this.closeFlyout()); DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) => field instanceof Doc && (CurrentUserUtils.GuestTarget = field))); } @@ -151,7 +176,8 @@ export class MainView extends React.Component { fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined, fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper, - fa.faBezierCurve, fa.faCircle, fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer); + fa.faDesktop, fa.faTrashRestore, fa.faUsers, fa.faWrench, fa.faCog, fa.faMap, fa.faBellSlash, fa.faExpandAlt, fa.faArchive, fa.faBezierCurve, fa.faCircle, + fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer, fa.faExpand, fa.faUndo, fa.faSlidersH, fa.faAngleDoubleLeft); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -227,7 +253,7 @@ export class MainView extends React.Component { const freeformOptions: DocumentOptions = { x: 0, y: 400, - _width: this._panelWidth * .7, + _width: this._panelWidth * .7 - this.propertiesWidth() * 0.7, _height: this._panelHeight, title: "Collection " + workspaceCount, }; @@ -293,16 +319,21 @@ export class MainView extends React.Component { @action onResize = (r: any) => { - this._panelWidth = r.offset.width; + this._panelWidth = r.offset.width;// - this.propertiesWidth(); this._panelHeight = r.offset.height; } - getPWidth = () => this._panelWidth; + + @action + getPWidth = () => this._panelWidth - this.propertiesWidth() + getPHeight = () => this._panelHeight; getContentsHeight = () => this._panelHeight - this._buttonBarHeight; - defaultBackgroundColors = (doc: Doc) => { + defaultBackgroundColors = (doc: Opt<Doc>) => { + if (this.panelContent === doc?.title) return "lightgrey"; if (this.darkScheme) { switch (doc?.type) { + case DocumentType.FONTICON: return "white"; case DocumentType.RTF || DocumentType.LABEL || DocumentType.BUTTON: return "#2d2d2d"; case DocumentType.LINK: case DocumentType.COL: { @@ -312,6 +343,7 @@ export class MainView extends React.Component { } } else { switch (doc?.type) { + case DocumentType.FONTICON: return "black"; case DocumentType.RTF: return "#f1efeb"; case DocumentType.BUTTON: case DocumentType.LABEL: return "lightgray"; @@ -324,40 +356,41 @@ export class MainView extends React.Component { } } - @computed get search() { - return <DocumentView Document={this.searchDoc!} - DataDoc={undefined} - LibraryPath={emptyPath} - addDocument={undefined} - addDocTab={this.addDocTabFunc} - pinToPres={emptyFunction} - rootSelected={returnTrue} - onClick={undefined} - backgroundColor={this.defaultBackgroundColors} - removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={this.getPWidth} - PanelHeight={this.getPHeight} - renderDepth={0} - focus={emptyFunction} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - />; - } + // @computed get search() { + // return <DocumentView Document={this.searchDoc!} + // DataDoc={undefined} + // LibraryPath={emptyPath} + // addDocument={undefined} + // addDocTab={this.addDocTabFunc} + // pinToPres={emptyFunction} + // rootSelected={returnTrue} + // onClick={undefined} + // backgroundColor={this.defaultBackgroundColors} + // removeDocument={undefined} + // ScreenToLocalTransform={Transform.Identity} + // ContentScaling={returnOne} + // NativeHeight={returnZero} + // NativeWidth={returnZero} + // PanelWidth={this.getPWidth} + // PanelHeight={this.getPHeight} + // renderDepth={0} + // focus={emptyFunction} + // parentActive={returnTrue} + // whenActiveChanged={emptyFunction} + // bringToFront={emptyFunction} + // docFilters={returnEmptyFilter} + // ContainingCollectionView={undefined} + // ContainingCollectionDoc={undefined} + // />; + // } @computed get mainDocView() { - return <DocumentView Document={this.mainContainer!} + return <DocumentView + Document={this.mainContainer!} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} @@ -373,7 +406,6 @@ export class MainView extends React.Component { NativeWidth={returnZero} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} - renderDepth={0} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} @@ -381,104 +413,67 @@ export class MainView extends React.Component { docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} + renderDepth={-1} />; } @computed get dockingContent() { TraceMobx(); const mainContainer = this.mainContainer; - const width = this.flyoutWidth; - return <Measure offset onResize={this.onResize}> - {({ measureRef }) => - <div ref={measureRef} className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> - {!mainContainer ? (null) : this.mainDocView} - </div> - } - </Measure>; - } - - _canClick = false; - onPointerDown = (e: React.PointerEvent) => { - if (this._flyoutTranslate) { - this._canClick = true; - this._flyoutSizeOnDown = 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(); - } + const width = this.flyoutWidth + this.propertiesWidth(); + return <div className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> + {!mainContainer ? (null) : this.mainDocView} + </div>; } @action - pointerLeaveDragger = () => { - if (!this._flyoutTranslate) { - this.flyoutWidth = 0; - this._flyoutTranslate = true; - } + onPropertiesPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + CurrentUserUtils.propertiesWidth = this._panelWidth - e.clientX; + return false; + }), returnFalse, action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false); } @action - onPointerMove = (e: PointerEvent) => { - this.flyoutWidth = Math.max(e.clientX, 0); - Math.abs(this.flyoutWidth - this._flyoutSizeOnDown) > 6 && (this._canClick = false); - this.sidebarButtonsDoc._columnWidth = this.flyoutWidth / 3 - 30; - } - @action - onPointerUp = (e: PointerEvent) => { - if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4 && this._canClick) { - this.flyoutWidth = this.flyoutWidth < 15 ? 250 : 0; - this.flyoutWidth && (this.sidebarButtonsDoc._columnWidth = this.flyoutWidth / 3 - 30); + onFlyoutPointerDown = (e: React.PointerEvent) => { + if (this._flyoutTranslate) { + setupMoveUpEvents(this, e, action((e: PointerEvent) => { + this.flyoutWidth = Math.max(e.clientX, 0); + if (this.flyoutWidth < 5) { + this.panelContent = "none"; + this._lastButton && (this._lastButton.color = "white"); + this._lastButton && (this._lastButton._backgroundColor = ""); + } + return false; + }), emptyFunction, action(() => { + if (this.flyoutWidth < 15) MainView.expandFlyout(); + else this.closeFlyout(); + })); } - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); } + flyoutWidthFunc = () => this.flyoutWidth; addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => { return where === "close" ? CollectionDockingView.CloseRightSplit(doc) : doc.dockingConfig ? this.openWorkspace(doc) : CollectionDockingView.AddRightSplit(doc, libraryPath); } - sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1); + sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0), 1); + //sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(0, -this._buttonBarHeight); + @computed get closePosition() { return 55 + this.flyoutWidth; } @computed get flyout() { - const sidebarContent = this.userDoc?.["tabs-panelContainer"]; - if (!(sidebarContent instanceof Doc)) { - return (null); - } - return <div className="mainView-flyoutContainer" > - <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight - 10/*margin-top*/}px`, backgroundColor: StrCast(this.sidebarButtonsDoc.backgroundColor) }}> - <DocumentView - Document={this.sidebarButtonsDoc} - DataDoc={undefined} - LibraryPath={emptyPath} - addDocument={undefined} - rootSelected={returnTrue} - addDocTab={this.addDocTabFunc} - pinToPres={emptyFunction} - removeDocument={undefined} - onClick={undefined} - ScreenToLocalTransform={this.sidebarScreenToLocal} - ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={this.flyoutWidthFunc} - PanelHeight={this.getPHeight} - renderDepth={0} - focus={emptyFunction} - backgroundColor={this.defaultBackgroundColors} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - </div> - <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "visible" }}> + if (!this.sidebarContent) return null; + return <div className="mainView-libraryFlyout"> + <div className="mainView-contentArea" style={{ position: "relative", height: `100%`, width: "100%", overflow: "visible" }}> + {this.flyoutWidth > 0 ? <div className="mainView-libraryFlyout-close" + onPointerDown={this.closeFlyout}> + <FontAwesomeIcon icon="times" color="black" size="lg" /> + </div> : null} + <DocumentView - Document={sidebarContent} + Document={this.sidebarContent} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} @@ -501,67 +496,214 @@ export class MainView extends React.Component { bringToFront={emptyFunction} docFilters={returnEmptyFilter} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - <div className="buttonContainer" > - <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> - <FontAwesomeIcon icon="cog" size="lg" /> - </button> - </div> + ContainingCollectionDoc={undefined} + relative={true} + /> </div> - {this.docButtons} + {this.docButtons}</div>; + } + + @computed get menuPanel() { + return <div className="mainView-menuPanel"> + <DocumentView + Document={Doc.UserDoc().menuStack as Doc} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={this.addDocTabFunc} + pinToPres={emptyFunction} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnTrue} + removeDocument={returnFalse} + onClick={undefined} + ScreenToLocalTransform={this.sidebarScreenToLocal} + ContentScaling={returnOne} + PanelWidth={() => 60} + PanelHeight={this.getContentsHeight} + renderDepth={0} + focus={emptyFunction} + backgroundColor={this.defaultBackgroundColors} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + relative={true} + scriptContext={this} + /> + </div>; + } + + + @action @undoBatch + closeFlyout = () => { + this._lastButton && (this._lastButton.color = "white"); + this._lastButton && (this._lastButton._backgroundColor = ""); + this.panelContent = "none"; + this.flyoutWidth = 0; + } + + get groupManager() { return GroupManager.Instance; } + + _lastButton: Doc | undefined; + @action @undoBatch + selectMenu = (button: Doc, str: string) => { + this._lastButton && (this._lastButton.color = "white"); + this._lastButton && (this._lastButton._backgroundColor = ""); + if (this.panelContent === str && this.flyoutWidth !== 0) { + this.panelContent = "none"; + this.flyoutWidth = 0; + } else { + let panelDoc: Doc | undefined; + switch (this.panelContent = str) { + case "Tools": panelDoc = Doc.UserDoc()["sidebar-tools"] as Doc ?? undefined; break; + case "Workspace": panelDoc = Doc.UserDoc()["sidebar-workspaces"] as Doc ?? undefined; break; + case "Catalog": panelDoc = Doc.UserDoc()["sidebar-catalog"] as Doc ?? undefined; break; + case "Archive": panelDoc = Doc.UserDoc()["sidebar-recentlyClosed"] as Doc ?? undefined; break; + case "Settings": SettingsManager.Instance.open(); break; + case "Sharing": GroupManager.Instance.open(); break; + case "UserDoc": panelDoc = Doc.UserDoc()["sidebar-userDoc"] as Doc ?? undefined; break; + } + this.sidebarContent.proto = panelDoc; + if (panelDoc) { + MainView.expandFlyout(); + button._backgroundColor = "lightgrey"; + button.color = "black"; + this._lastButton = button; + } else this.flyoutWidth = 0; + } + return true; + } + + @action @undoBatch + closeProperties = () => { + CurrentUserUtils.propertiesWidth = 0; + } + + @computed get propertiesView() { + TraceMobx(); + return <div className="mainView-propertiesView" style={{ + overflow: this.propertiesWidth() < 15 ? "hidden" : undefined + }}> + <PropertiesView + width={this.propertiesWidth()} + height={this._panelHeight} + renderDepth={1} + ScreenToLocalTransform={Transform.Identity} + onDown={this.closeProperties} + /> </div>; } + @computed get mainInnerContent() { + const rightFlyout = this.propertiesWidth() - 1; + return <> + {this.menuPanel} + <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> + <div className="mainView-flyoutContainer" style={{ width: this.flyoutWidth }}> + {this.flyoutWidth !== 0 ? <div className="mainView-libraryHandle" + onPointerDown={this.onFlyoutPointerDown} + style={{ backgroundColor: 'lightgrey' }}> + <span title="library View Dragger" style={{ + width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", + //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", + position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", + top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" + }} /> + <div className="mainview-libraryHandle-icon"> + <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> </div> + </div> : null} + <div className="mainView-libraryFlyout" style={{ + //transformOrigin: this._flyoutTranslate ? "" : "left center", + transition: this._flyoutTranslate ? "" : "width .5s", + //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, + boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw" + }}> + {this.flyout} + {this.expandButton} + </div> + </div> + {this.dockingContent} + {this.showProperties ? (null) : + <div className="mainView-propertiesDragger" title="Properties View Dragger" onPointerDown={this.onPropertiesPointerDown} + style={{ right: rightFlyout, top: "50%" }}> + <div className="mainView-propertiesDragger-icon"> + <FontAwesomeIcon icon={this.propertiesIcon} color="white" size="sm" /> </div> + </div> + } + {this.propertiesWidth() < 10 ? (null) : + <div style={{ width: this.propertiesWidth() }}> {this.propertiesView} </div>} + </div> + </>; + } + @computed get mainContent() { - const sidebar = this.userDoc?.["tabs-panelContainer"]; - const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); + //const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); + const n = (CollectionMenu.Instance?.Pinned ? 1 : 0); const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; - return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( - <div> - <div style={{ height: "32px", width: "100%", backgroundColor: "black" }}> - {this.search} - </div> - <div className="mainView-mainContent" style={{ - color: this.darkScheme ? "rgb(205,205,205)" : "black", - //change to times 2 for both pinned - height, - width: (FormatShapePane.Instance?.Pinned) ? `calc(100% - 200px)` : "100%" - }} > - <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> - <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> - <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} - style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}> - <span title="library View Dragger" style={{ - width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", - //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", - position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", - top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" - }} /> - </div> - <div className="mainView-libraryFlyout" style={{ - //transformOrigin: this._flyoutTranslate ? "" : "left center", - transition: this._flyoutTranslate ? "" : "width .5s", - //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, - boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw" - }}> - {this.flyout} - {this.expandButton} - </div> - </div> - {this.dockingContent} + // return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( + // <div> + // <div style={{ height: "32px", width: "100%", backgroundColor: "black" }}> + // {this.search} + // </div> + // <div className="mainView-mainContent" style={{ + // color: this.darkScheme ? "rgb(205,205,205)" : "black", + // //change to times 2 for both pinned + // height, + // width: (FormatShapePane.Instance?.Pinned) ? `calc(100% - 200px)` : "100%" + // }} > + // <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> + // <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> + // <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} + // style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}> + // <span title="library View Dragger" style={{ + // width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", + // //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", + // position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", + // top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" + // }} /> + // </div> + // <div className="mainView-libraryFlyout" style={{ + // //transformOrigin: this._flyoutTranslate ? "" : "left center", + // transition: this._flyoutTranslate ? "" : "width .5s", + // //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, + // boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw" + // }}> + // {this.flyout} + // {this.expandButton} + // </div> + // </div> + // {this.dockingContent} + // </div> + // </div> + // </div>); + const pinned = FormatShapePane.Instance?.Pinned; + const innerContent = this.mainInnerContent; + return !this.userDoc ? (null) : ( + <Measure offset onResize={this.onResize}> + {({ measureRef }) => + <div className="mainView-mainContent" ref={measureRef} style={{ + color: this.darkScheme ? "rgb(205,205,205)" : "black", + //change to times 2 for both pinned + height, + width: pinned ? `calc(100% - 200px)` : "100%" + }} > + {innerContent} </div> - </div> - </div>); + } + </Measure>); } public static expandFlyout = action(() => { MainView.Instance._flyoutTranslate = true; MainView.Instance.flyoutWidth = (MainView.Instance.flyoutWidth || 250); - MainView.Instance.sidebarButtonsDoc._columnWidth = MainView.Instance.flyoutWidth / 3 - 30; + }); @computed get expandButton() { - return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); + return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}></div>) : (null); } addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); @@ -586,6 +728,7 @@ export class MainView extends React.Component { fieldKey={"data"} dropAction={"alias"} annotationsKey={""} + backgroundColor={this.defaultBackgroundColors} rootSelected={returnTrue} bringToFront={emptyFunction} select={emptyFunction} @@ -655,8 +798,40 @@ export class MainView extends React.Component { </svg>; } + @computed get search() { + return <div className="mainView-searchPanel"> + {/* <div style={{ float: "left", marginLeft: "10px" }}>{Doc.CurrentUserEmail}</div> */} + <div><DocumentView Document={this.searchDoc!} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={this.addDocTabFunc} + pinToPres={emptyFunction} + rootSelected={returnTrue} + onClick={undefined} + backgroundColor={this.defaultBackgroundColors} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} + PanelWidth={this.getPWidth} + PanelHeight={this.getPHeight} + renderDepth={0} + focus={emptyFunction} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /></div> + </div>; + } + render() { return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> + {this.inkResources} <DictationOverlay /> <SharingManager /> @@ -665,9 +840,10 @@ export class MainView extends React.Component { <GoogleAuthenticationManager /> <HypothesisAuthenticationManager /> <DocumentDecorations /> + {this.search} <CollectionMenu /> <FormatShapePane /> - <RichTextMenu key="rich" /> + <div style={{ display: "none" }}><RichTextMenu key="rich" /></div> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 249715511..66ea2dbf8 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -10,7 +10,7 @@ export interface MainViewOverlayProps { overlayStyle?: React.CSSProperties; dialogueBoxDisplayedOpacity?: number; overlayDisplayedOpacity?: number; - closeOnExternalClick?: () => void; + closeOnExternalClick?: () => void; // the close method of a MainViewModal, triggered if there is a click on the overlay (closing the modal) } @observer diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index b4116e980..d7034fcfb 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -112,10 +112,10 @@ export class PreviewCursor extends React.Component<{}> { } else if (e.clipboardData.items.length) { const batch = UndoManager.StartBatch("collection view drop"); const files: File[] = []; - for (let i = 0; i < e.clipboardData.items.length; i++) { - const file = e.clipboardData.items[i].getAsFile(); + Array.from(e.clipboardData.items).forEach(item => { + const file = item.getAsFile(); file && files.push(file); - } + }); const generatedDocuments = await DocUtils.uploadFilesToDocs(files, { x: newPoint[0], y: newPoint[1] }); generatedDocuments.forEach(PreviewCursor._addDocument); batch.end(); diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss new file mode 100644 index 000000000..1cba252de --- /dev/null +++ b/src/client/views/PropertiesButtons.scss @@ -0,0 +1,129 @@ +@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: 30px; + width: 30px; + border-radius: 5px; + opacity: 0.9; + pointer-events: auto; + background-color: #121721; + color: #fcfbf7; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.propertiesButtons { + margin-top: 3px; + grid-column: 1/4; + width: 100%; + height: auto; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.onClickFlyout-editScript { + text-align: center; + border: 0.5px solid grey; + background-color: rgb(230, 230, 230); + border-radius: 9px; + padding: 4px; +} + + +.propertiesButtons-button { + pointer-events: auto; + padding-right: 5px; + width: 25px; + border-radius: 5px; + margin-right: 18px; + margin-bottom: 8px; +} + +.propertiesButtons-linker { + height: 30px; + width: 30px; + text-align: center; + border-radius: 5px; + pointer-events: auto; + // color: $dark-color; + // border: $dark-color 1px solid; + background-color: #252b33; + color: #fcfbf7; + transition: 0.2s ease all; + margin-right: 5px; + padding-top: 5px; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.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..d46c03470 --- /dev/null +++ b/src/client/views/PropertiesButtons.tsx @@ -0,0 +1,658 @@ +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, BoolCast } from "../../fields/Types"; +import { emptyFunction, setupMoveUpEvents, Utils } 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'; +import { SelectionManager } from '../util/SelectionManager'; +import SharingManager from '../util/SharingManager'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../fields/URLField'; +import { undoBatch, UndoManager } from '../util/UndoManager'; +import { DocumentType } from '../documents/DocumentTypes'; +import { CollectionFreeFormView } from './collections/collectionFreeForm/CollectionFreeFormView'; +import { InkField } from '../../fields/InkField'; +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<{}, {}> { + 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; + + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } + + @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; } + + 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; + }); + + @computed + get considerGoogleDocsPush() { + const targetDoc = this.selectedDoc; + 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.selectedDoc; + 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.selectedDoc; + 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 ? "white" : "", color: isPinned ? "black" : "white" }} + onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" + /> + </div></Tooltip>; + } + + @computed + get metadataButton() { + //const view0 = this.view0; + if (this.selectedDoc) { + return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={<MetadataEntryMenu docs={[this.selectedDoc]} 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>; + } else { + return null; + } + + } + + @observable _aliasDown = false; + onAliasButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + } + @undoBatch + onAliasButtonMoved = () => { + if (this._dragRef.current) { + const dragDocView = this.selectedDocumentView!; + 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 docView = this.selectedDocumentView; + const templates: Map<Template, boolean> = new Map(); + const views = [this.selectedDocumentView]; + 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 !docView ? (null) : + <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} //onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} + content={<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + <div className={"propertiesButtons-linkButton-empty"} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; + } + + @undoBatch + onCopy = () => { + if (this.selectedDoc && this.selectedDocumentView) { + // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); + // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + // copy.y = NumCast(this.selectedDoc.y) + 30; + // this.selectedDocumentView.props.addDocument?.(copy); + const alias = Doc.MakeAlias(this.selectedDoc); + alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + alias.y = NumCast(this.selectedDoc.y) + 30; + this.selectedDocumentView.props.addDocument?.(alias); + } + } + + @computed + get copyButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + ref={this._dragRef} + onPointerDown={this.onAliasButtonDown} + onClick={this.onCopy}> + {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="sm" />} + </div> + </Tooltip>; + } + + @action @undoBatch + onLock = () => { + this.selectedDocumentView?.toggleLockPosition(); + } + + @computed + get lockButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{this.selectedDoc?.lockedPosition ? + "Unlock Position" : "Lock Position"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={this.onLock} > + {<FontAwesomeIcon className="documentdecorations-icon" + icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="sm" />} + </div> + </Tooltip>; + } + + @computed + get downloadButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Download Document"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={async () => { + if (this.selectedDocumentView?.props.Document) { + Doc.Zip(this.selectedDocumentView?.props.Document); + } + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="download" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get deleteButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Delete Document"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={this.deleteDocument}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="trash-alt" size="sm" />} + </div> + </Tooltip>; + } + + @undoBatch + @action + deleteDocument = () => { + this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document); + } + + @computed + get sharingButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Share Document"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + if (this.selectedDocumentView) { + SharingManager.Instance.open(this.selectedDocumentView); + } + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="users" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get onClickButton() { + if (this.selectedDoc) { + return <Tooltip title={<><div className="dash-tooltip">Choose onClick behavior</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={this.onClickFlyout}> + <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; + } else { + return null; + } + } + + @undoBatch + @action + handleOptionChange = (e: any) => { + const value = e.target.value; + this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); + if (value === "nothing") { + this.selectedDocumentView?.noOnClick(); + } else if (value === "enterPortal") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.makeIntoPortal(); + } else if (value === "toggleDetail") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.toggleDetail(); + } else if (value === "linkInPlace") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.toggleFollowLink("inPlace", true, false); + } else if (value === "linkOnRight") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.toggleFollowLink("onRight", false, false); + } + } + + @undoBatch @action + editOnClickScript = () => { + if (this.selectedDoc) { + DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); + } + } + + @computed + get onClickFlyout() { + return <div><form> + <div className="radio"> + <label> + <input type="radio" value="nothing" + checked={this.onClick === 'nothing'} + onChange={this.handleOptionChange} /> + Select Document + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="enterPortal" + checked={this.onClick === 'enterPortal'} + onChange={this.handleOptionChange} /> + Enter Portal + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="toggleDetail" + checked={this.onClick === 'toggleDetail'} + onChange={this.handleOptionChange} /> + Toggle Detail + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="linkInPlace" + checked={this.onClick === 'linkInPlace'} + onChange={this.handleOptionChange} /> + Follow Link + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="linkOnRight" + checked={this.onClick === 'linkOnRight'} + onChange={this.handleOptionChange} /> + Open Link on Right + </label> + </div> + </form> + <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div> + </div>; + } + + @computed + get googlePhotosButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + if (this.selectedDocumentView) { + GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log); + } + }}> + {<FontAwesomeIcon className="documentdecorations-icon" + icon="cloud-upload-alt" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get clustersButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{this.selectedDoc?.useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + style={{ backgroundColor: this.selectedDoc?.useClusters ? "#a0a0a0" : "" }} + onPointerDown={this.changeClusters}> + {<FontAwesomeIcon className="documentdecorations-icon" + color={this.selectedDoc?.useClusters ? "black" : "white"} + icon="braille" size="sm" />} + </div> + </Tooltip>; + } + + @action @undoBatch + changeFitToBox = () => { + this.selectedDoc && (this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox); + } + + @action @undoBatch + changeClusters = () => { + this.selectedDoc && (this.selectedDoc.useClusters = !this.selectedDoc.useClusters); + } + + @computed + get fitContentButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{this.selectedDoc?._fitToBox ? "Stop Fitting Content" : "Fit Content"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + style={{ backgroundColor: this.selectedDoc?._fitToBox ? "#a0a0a0" : "" }} + onPointerDown={this.changeFitToBox}> + {<FontAwesomeIcon className="documentdecorations-icon" + color={this.selectedDoc?._fitToBox ? "black" : "white"} + icon="expand" size="sm" />} + </div> + </Tooltip>; + } + + @undoBatch + @action + private makeMask = () => { + if (this.selectedDoc) { + this.selectedDoc._backgroundColor = "rgba(0,0,0,0.7)"; + this.selectedDoc.mixBlendMode = "hard-light"; + this.selectedDoc.color = "#9b9b9bff"; + this.selectedDoc.stayInCollection = true; + this.selectedDoc.isInkMask = true; + } + } + + @computed + get maskButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">Make Mask</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={this.makeMask}> + {<FontAwesomeIcon className="documentdecorations-icon" + color="white" icon="paint-brush" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get contextButton() { + if (this.selectedDoc) { + return <Tooltip title={<><div className="dash-tooltip">Show Context</div></>}> + <div className={"propertiesButtons-linkButton-empty"}> + <ParentDocSelector Document={this.selectedDoc} addDocTab={(doc, where) => { + where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : + this.selectedDocumentView?.props.addDocTab(doc, "onRight"); + return true; + }} /> + </div> + </Tooltip>; + } else { + return false; + } + + } + + // @computed + // get importButton() { + // const targetDoc = this.selectedDoc; + // return !targetDoc ? (null) : <Tooltip + // title={<><div className="dash-tooltip">{"Import a Document"}</div></>}> + // <div className={"propertiesButtons-linkButton-empty"} + // onPointerDown={() => { + // if (this.selectedDocumentView) { + // CollectionFreeFormView.importDocument(100, 100); + // } + // }}> + // {<FontAwesomeIcon className="documentdecorations-icon" + // icon="upload" size="sm" />} + // </div> + // </Tooltip>; + // } + + + render() { + if (!this.selectedDoc) return (null); + + const isText = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + const considerPull = isText && this.considerGoogleDocsPull; + const considerPush = isText && this.considerGoogleDocsPush; + const isImage = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof ImageField; + const isInk = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof InkField; + const isCollection = this.selectedDoc.type === DocumentType.COL ? true : false; + const isFreeForm = this.selectedDoc._viewType === "freeform" ? true : false; + + return <div><div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> + <div className="propertiesButtons-button"> + {this.templateButton} + </div> + {/* <div className="propertiesButtons-button"> + {this.metadataButton} + </div> */} + <div className="propertiesButtons-button"> + {this.pinButton} + </div> + <div className="propertiesButtons-button"> + {this.copyButton} + </div> + <div className="propertiesButtons-button"> + {this.lockButton} + </div> + <div className="propertiesButtons-button"> + {this.downloadButton} + </div> + <div className="propertiesButtons-button"> + {this.deleteButton} + </div> + <div className="propertiesButtons-button"> + {this.onClickButton} + </div> + {/* <div className="propertiesButtons-button"> + {this.contextButton} + </div> */} + <div className="propertiesButtons-button"> + {this.sharingButton} + </div> + <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}> + {this.considerGoogleDocsPush} + </div> + <div className="propertiesButtons-button" style={{ display: !considerPull ? "none" : "" }}> + {this.considerGoogleDocsPull} + </div> + <div className="propertiesButtons-button" style={{ display: !isImage ? "none" : "" }}> + {this.googlePhotosButton} + </div> + {/* <div className="propertiesButtons-button" style={{ display: !isCollection ? "none" : "" }}> + {this.importButton} + </div> */} + + <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> + {this.clustersButton} + </div> + + <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> + {this.fitContentButton} + </div> + + <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> + {this.maskButton} + </div> + </div> + </div>; + } +} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 1895c06a1..6ebd5103b 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -12,12 +12,14 @@ width: 100%; height: 100%; position: absolute; + .miniThumb { background: #25252525; position: absolute; } } } + .lm_title { margin-top: 3px; border-radius: 5px; @@ -27,6 +29,7 @@ transform: translate(0px, -3px); cursor: grab; } + .lm_title.focus-visible { cursor: text; } @@ -34,23 +37,39 @@ .lm_title_wrap { overflow: hidden; height: 19px; - margin-top: -3px; - display:inline-block; + margin-top: -2px; + display: inline-block; } + .lm_active .lm_title { border: solid 1px lightgray; } + .lm_header .lm_tab .lm_close_tab { position: absolute; text-align: center; } .lm_header .lm_tab { - padding-right : 20px; + padding-right: 20px; + margin-top: -1px; + border-bottom: 1px black; + .collectionDockingView-gear { + display: none; + } +} + +.lm_header .lm_tab.lm_active { + padding-right: 20px; + margin-top: 1px; + border-bottom: unset; + .collectionDockingView-gear { + display: inline-block; + } } .lm_popout { - display:none; + display: none; } .messageCounter { @@ -73,14 +92,15 @@ position: absolute; top: 0; left: 0; + // overflow: hidden; // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu) .collectionDockingView-gear { padding-left: 5px; height: 15px; width: 18px; - display: inline-block; margin: auto; } + .collectionDockingView-dragAsDocument { touch-action: none; position: absolute; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 4952dedc9..533c8bffe 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,9 +1,8 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, Lambda, observable, reaction, runInAction, trace } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction, trace, IReactionDisposer } 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 '../../../fields/DateField'; import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; @@ -31,6 +30,8 @@ import { SnappingManager } from '../../util/SnappingManager'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { listSpec } from '../../../fields/Schema'; import { clamp } from 'lodash'; +import { InteractionUtils } from '../../util/InteractionUtils'; +import { InkTool } from '../../../fields/InkField'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -464,6 +465,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { this._flush = true; } + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { + return; + } else { + e.stopPropagation(); + } } updateDataField = async (json: string) => { @@ -505,7 +511,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; if (doc instanceof Doc) { - //tab.titleElement[0].outerHTML = `<input class='lm_title' style="background:black" value='${doc.title}' />`; tab.titleElement[0].onclick = (e: any) => tab.titleElement[0].focus(); tab.titleElement[0].onchange = (e: any) => { tab.titleElement[0].size = e.currentTarget.value.length + 1; @@ -520,6 +525,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp gearSpan.style.paddingLeft = "0px"; gearSpan.style.paddingRight = "12px"; const stack = tab.contentItem.parent; + tab.element[0].onpointerdown = (e: any) => { + const view = DocumentManager.Instance.getDocumentView(doc); + view && SelectionManager.SelectDoc(view, false); + }; // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: any) => { if (!this._isPointerDown || !SnappingManager.GetIsDragging()) return; @@ -674,10 +683,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @observable private _isActive: boolean = false; + _tabReaction: IReactionDisposer | undefined; get _stack(): any { return (this.props as any).glContainer.parent.parent; } + get _tab(): any { + const tab = (this.props as any).glContainer.tab.element[0] as HTMLElement; + return tab.getElementsByClassName("lm_title")?.[0]; + } constructor(props: any) { super(props); DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); @@ -738,9 +752,16 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.on("tab", this.onActiveContentItemChanged); this.onActiveContentItemChanged(); + this._tabReaction = reaction(() => ({ views: SelectionManager.SelectedDocuments(), color: StrCast(this._document?._backgroundColor, "white") }), + (data) => { + const selected = data.views.some(v => Doc.AreProtosEqual(v.props.Document, this._document)); + this._tab.style.backgroundColor = selected ? data.color : ""; + } + ); } componentWillUnmount() { + this._tabReaction?.(); this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.off("tab", this.onActiveContentItemChanged); } @@ -749,6 +770,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { private onActiveContentItemChanged() { if (this.props.glContainer.tab) { this._isActive = this.props.glContainer.tab.isActive; + setTimeout(() => { + const dv = this._document && DocumentManager.Instance.getFirstDocumentView(this._document); + dv && SelectionManager.SelectDoc(dv, false); + }); !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } } diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 3b31947f7..a9e812ad3 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -161,7 +161,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} renderDepth={this.props.renderDepth + 1} focus={emptyFunction} - backgroundColor={returnEmptyString} + backgroundColor={this.props.backgroundColor} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 9a7ea2c93..c772dcfe7 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -238,7 +238,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr contents: "+ NEW", HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this.color }; const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled'); const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; @@ -278,7 +277,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr oneLine: true, HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this.color }; return this.props.parent.props.Document.miniHeaders ? <div className="collectionStackingView-miniHeader"> diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 9da204787..0a316317f 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -2,8 +2,8 @@ .collectionMenu-cont { - position:relative; - display:inline-flex; + position: relative; + display: inline-flex; width: 100%; opacity: 0.9; z-index: 9001; @@ -12,14 +12,15 @@ color: white; transform-origin: top left; top: 0; - width:100%; + width: 100%; .antimodeMenu-button { padding: 0; width: 30px; display: flex; + svg { - margin:auto; + margin: auto; } } @@ -108,6 +109,7 @@ margin-top: auto; margin-bottom: auto; } + .collectionViewBaseChrome-viewSpecs { margin-left: 5px; display: grid; @@ -318,12 +320,14 @@ text-align: center; display: block; } + .color-previewI { width: 80%; height: 20%; bottom: 0; position: absolute; } + .color-previewII { width: 80%; height: 80%; @@ -336,7 +340,7 @@ margin: auto; /* Make the buttons appear below each other */ } - + .btn-draw { display: inline-flex; margin: auto; @@ -374,6 +378,7 @@ display: block; margin: auto; } + border-right: solid gray 1px; } } @@ -398,14 +403,14 @@ .collectionSchemaViewChrome-toggler { width: 100px; - height: 41px; + height: 35px; background-color: black; position: relative; } .collectionSchemaViewChrome-togglerButton { width: 47px; - height: 35px; + height: 30px; background-color: $light-color-secondary; // position: absolute; transition: all 0.5s ease; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 0ca86172f..a7d2c07fa 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -25,9 +25,13 @@ import { SelectionManager } from "../../util/SelectionManager"; import { DocumentView } from "../nodes/DocumentView"; import { ColorState } from "react-color"; import { ObjectField } from "../../../fields/ObjectField"; +import RichTextMenu from "../nodes/formattedText/RichTextMenu"; +import { RichTextField } from "../../../fields/RichTextField"; import { ScriptField } from "../../../fields/ScriptField"; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { DocUtils } from "../../documents/Documents"; +import { Tooltip } from "@material-ui/core"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @observer export default class CollectionMenu extends AntimodeMenu { @@ -47,7 +51,7 @@ export default class CollectionMenu extends AntimodeMenu { componentDidMount() { reaction(() => SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0], - (doc) => doc && this.SetSelection(doc)) + (doc) => doc && this.SetSelection(doc)); } @action @@ -63,16 +67,37 @@ export default class CollectionMenu extends AntimodeMenu { } } + @action + toggleProperties = () => { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } else { + CurrentUserUtils.propertiesWidth = 250; + } + } + render() { - const button = <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - </button>; + const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> + <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> + </button> + </Tooltip>; + + const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; + const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Properties Panel" : "Open Properties Panel"; + + const prop = <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="properties" placement="bottom"> + <button className="antimodeMenu-button" key="properties" onPointerDown={this.toggleProperties}> + <FontAwesomeIcon icon={propIcon} size="lg" /> + </button> + </Tooltip>; return this.getElement(!this.SelectedCollection ? [button] : [<CollectionViewBaseChrome key="chrome" docView={this.SelectedCollection} fieldKey={Doc.LayoutFieldKey(this.SelectedCollection?.props.Document)} type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, + prop, button]); } } @@ -160,12 +185,12 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; }, }; - _freeform_commands = [this._viewCommand, this._saveFilterCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; - _stacking_commands = [this._contentCommand, this._templateCommand]; - _masonry_commands = [this._contentCommand, this._templateCommand]; - _schema_commands = [this._templateCommand, this._narrativeCommand]; - _doc_commands = [this._openLinkInCommand, this._onClickCommand]; - _tree_commands = []; + @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } + @computed get _stacking_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } + @computed get _masonry_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } + @computed get _schema_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } + @computed get _doc_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._openLinkInCommand, this._onClickCommand]; } + @computed get _tree_commands() { return undefined; } private get _buttonizableCommands() { switch (this.props.type) { default: return this._doc_commands; @@ -185,7 +210,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @observable private _currentKey: string = ""; componentDidMount = action(() => { - this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : ""); + this._currentKey = this._currentKey || (this._buttonizableCommands?.length ? this._buttonizableCommands[0]?.title : ""); }); @undoBatch @@ -235,7 +260,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp protected drop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; if (docDragData?.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); + this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); } return true; @@ -257,76 +282,100 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } dragCommandDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, (e, down, delta) => { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => + this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.document }, c.params, c.initialize, e.clientX, e.clientY)); return true; }, emptyFunction, () => { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([])); + this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate([])); }); } @computed get templateChrome() { return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > - <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <button className={"antimodeMenu-button"} > - <FontAwesomeIcon icon="bullseye" size="lg" /> - </button> - <select - className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> - {this._buttonizableCommands.map(cmd => - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> - )} - </select> - </div> + <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> + <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> + <button className={"antimodeMenu-button"} > + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> + <select + className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> + {this._buttonizableCommands?.map(cmd => + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> + )} + </select> + </div> + </Tooltip> </div>; } @computed get viewModes() { return <div className="collectionViewBaseChrome-viewModes" > - <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> - <button className={"antimodeMenu-button"}> - <FontAwesomeIcon icon="bullseye" size="lg" /> - </button> - <select - className="collectionViewBaseChrome-viewPicker" - onPointerDown={stopPropagation} - onChange={this.viewChanged} - value={StrCast(this.props.type)}> - {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( - <option - key={Utils.GenerateGuid()} - className="collectionViewBaseChrome-viewOption" - onPointerDown={stopPropagation} - value={type}> - {type[0].toUpperCase() + type.substring(1)} - </option> - ))} - </select> - </div> + <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> + <div className="commandEntry-outerDiv" ref={this._viewRef} onPointerDown={this.dragViewDown}> + <button className={"antimodeMenu-button"}> + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> + <select + className="collectionViewBaseChrome-viewPicker" + onPointerDown={stopPropagation} + onChange={this.viewChanged} + value={StrCast(this.props.type)}> + {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( + <option + key={Utils.GenerateGuid()} + className="collectionViewBaseChrome-viewOption" + onPointerDown={stopPropagation} + value={type}> + {type[0].toUpperCase() + type.substring(1)} + </option> + ))} + </select> + </div> + </Tooltip> </div>; } + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get notACollection() { + if (this.selectedDoc) { + const layoutField = Doc.LayoutField(this.selectedDoc); + return this.props.type === CollectionViewType.Docking || + typeof (layoutField) === "string" && !layoutField?.includes("CollectionView"); + } + else return false; + } + render() { return ( <div className="collectionMenu-cont" > <div className="collectionMenu"> <div className="collectionViewBaseChrome"> - {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.viewModes} - {this.props.type === CollectionViewType.Docking ? (null) : this.templateChrome} - <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: "grid" }}> - <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > - <FontAwesomeIcon icon="filter" size="lg" /> - </button> - </div> - - {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <button className={"antimodeMenu-button"} key="float" - style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} - title="Toggle Overlay Layer" - onClick={() => DocumentView.FloatDoc(this.props.docView)}> - <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> - </button>} + {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} + {!this._buttonizableCommands ? (null) : this.templateChrome} + {Doc.UserDoc().noviceMode ? (null) : + <Tooltip title={<div className="dash-tooltip">filter documents to show</div>} placement="bottom"> + <div className="collectionViewBaseChrome-viewSpecs" style={{ display: "grid" }}> + <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > + <FontAwesomeIcon icon="filter" size="lg" /> + </button> + </div> + </Tooltip>} + + {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : + <Tooltip title={<div className="dash-tooltip">Toggle Overlay Layer</div>} placement="bottom"> + <button className={"antimodeMenu-button"} key="float" + style={{ backgroundColor: this.props.docView.layoutDoc.z ? "121212" : undefined, borderRight: "1px solid gray" }} + onClick={() => DocumentView.FloatDoc(this.props.docView)}> + <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> + </button> + </Tooltip>} </div> {this.subChrome} </div> @@ -356,6 +405,20 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @computed get childDocs() { return DocListCast(this.dataField); } + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get isText() { + if (this.selectedDoc) { + return this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + } + else return false; + } + @undoBatch @action nextKeyframe = (): void => { @@ -384,12 +447,9 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu miniMap = (): void => { this.document.hideMinimap = !this.document.hideMinimap; } + private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; private _width = ["1", "5", "10", "100"]; - // private _draw = ["⎯", "→", "↔︎", "∿", "↝", "↭", "ロ", "O", "∆"]; - // private _head = ["", "", "arrow", "", "", "arrow", "", "", ""]; - // private _end = ["", "arrow", "arrow", "", "arrow", "arrow", "", "", ""]; - // private _shape = ["line", "line", "line", "", "", "", "rectangle", "circle", "triangle"]; private _dotsize = [10, 20, 30, 40]; private _draw = ["∿", "⎯", "→", "↔︎", "ロ", "O"]; private _head = ["", "", "", "arrow", "", ""]; @@ -459,22 +519,24 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu }); return <div className="btn-draw" key="draw"> {this._draw.map((icon, i) => - <button className="antimodeMenu-button" title={this._title[i]} key={icon} onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} - style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> - {/* {this._draw[i]} */} - <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" /> - - </button>)} + <Tooltip key={icon} title={<div className="dash-tooltip">{this._title[i]}</div>} placement="bottom"> + <button className="antimodeMenu-button" onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} + style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> + <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" /> + </button> + </Tooltip>)} </div>; } toggleButton = (key: string, value: boolean, setter: () => {}, icon: FontAwesomeIconProps["icon"], ele: JSX.Element | null) => { - return <button className="antimodeMenu-button" key={key} title={key} - onPointerDown={action(e => setter())} - style={{ backgroundColor: value ? "121212" : "" }}> - <FontAwesomeIcon icon={icon} size="lg" /> - {ele} - </button>; + return <Tooltip title={<div className="dash-tooltip">{key}</div>} placement="bottom"> + <button className="antimodeMenu-button" key={key} + onPointerDown={action(e => setter())} + style={{ backgroundColor: value ? "121212" : "" }}> + <FontAwesomeIcon icon={icon} size="lg" /> + {ele} + </button> + </Tooltip>; } @computed get widthPicker() { @@ -483,11 +545,13 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu <div className="btn2-group" key="width"> {widthPicker} {this._width.map((wid, i) => - <button className="antimodeMenu-button" key={wid} title="change width" - onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} - style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}> - • - </button>)} + <Tooltip title={<div className="dash-tooltip">change width</div>} placement="bottom"> + <button className="antimodeMenu-button" key={wid} + onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} + style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}> + • + </button> + </Tooltip>)} </div>; } @@ -522,51 +586,56 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </div>; } - @computed get formatPane() { - return <button className="antimodeMenu-button" key="format" title="toggle foramatting pane" - onPointerDown={action(e => FormatShapePane.Instance.Pinned = !FormatShapePane.Instance.Pinned)} - style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="angle-double-right" size="lg" /> - </button>; - } - render() { - return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> - {this.props.docView.props.renderDepth !== 0 ? (null) : - <div key="map" title="mini map" className="backKeyframe" onClick={this.miniMap}> - <FontAwesomeIcon icon={"map"} size={"lg"} /> - </div> - } - <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}> - <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> - </div> - <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} - onClick={action(() => this.document.editing = !this.document.editing)} > - {NumCast(this.document.currentFrame)} - </div> - <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> - <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> - </div> - - {!this.props.isOverlay || this.document.type !== DocumentType.WEB ? (null) : - <button className={"antimodeMenu-button"} key="hypothesis" - style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} - title="Use Hypothesis" - onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> - <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> - </button> - } - {!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating ? - <> - {this.drawButtons} - {this.widthPicker} - {this.colorPicker} - {this.fillPicker} - {this.formatPane} - </> : - (null) - } - </div>; + return !this.props.docView.layoutDoc ? (null) : + <div className="collectionFreeFormMenu-cont"> + {this.props.docView.props.renderDepth !== 0 || this.isText ? (null) : + <Tooltip key="map" title={<div className="dash-tooltip">Toggle Mini Map</div>} placement="bottom"> + <div className="backKeyframe" onClick={this.miniMap}> + <FontAwesomeIcon icon={"map"} size={"lg"} /> + </div> + </Tooltip> + } + {!this.isText ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> + <div className="backKeyframe" onClick={this.prevKeyframe}> + <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> + </div> + </Tooltip> : null} + {!this.isText ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + <div className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} + onClick={action(() => this.document.editing = !this.document.editing)} > + {NumCast(this.document.currentFrame)} + </div> + </Tooltip> : null} + {!this.isText ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> + <div className="fwdKeyframe" onClick={this.nextKeyframe}> + <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> + </div> + </Tooltip> : null} + + {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText ? (null) : + <Tooltip key="hypothesis" title={<div className="dash-tooltip">Use Hypothesis</div>} placement="bottom"> + <button className={"antimodeMenu-button"} key="hypothesis" + style={{ + backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, + borderRight: "1px solid gray" + }} + onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> + <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> + </button> + </Tooltip> + } + {(!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating) && !this.isText ? + <> + {this.drawButtons} + {this.widthPicker} + {this.colorPicker} + {this.fillPicker} + </> : + (null) + } + {this.isText ? <RichTextMenu key="rich" /> : null} + </div>; } } @observer diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 11382b722..b93a7f7b8 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -257,7 +257,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchema-headerMenu-group"> <div onClick={() => this.typesDropdownChange(!this._openTypes)}> <label>Column type:</label> - <FontAwesomeIcon icon={"caret-down"} size="sm" style={{ float: "right" }} /> + <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right" }} /> </div> {this._openTypes ? allColumnTypes : justColType} </div > diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 1c5cf4290..4998aaaae 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -230,6 +230,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) addDocTab={this.addDocTab} bringToFront={returnFalse} ContentScaling={returnOne} + scriptContext={this.props.scriptContext} pinToPres={this.props.pinToPres} />; } @@ -484,7 +485,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) })} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} - onWheel={e => this.props.active() && e.stopPropagation()} > + onWheel={e => this.props.active(true) && e.stopPropagation()} > {this.renderedSections} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 76af70cd1..4042a070d 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -298,7 +298,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC oneLine: true, HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this._color }; const newEditableViewProps = { GetValue: () => "", @@ -306,7 +305,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC contents: "+ NEW", HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this._color }; const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} @@ -332,11 +330,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this._paletteOn ? this.renderColorPicker() : (null)} </div> } - {evContents === `NO ${key.toUpperCase()} VALUE` ? - (null) : - <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> - <FontAwesomeIcon icon="trash" size="lg" /> - </button>} + {<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button>} {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionOptions"> <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> @@ -355,7 +351,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} { this.collapsed ? (null) : - <div style={{ marginTop: 5 }}> + <div> <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} style={{ padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 4463eef3e..ab28e1904 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -194,13 +194,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: @action protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; - ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); if (docDragData) { let added = false; - const dropaction = docDragData.dropAction || docDragData.userDropAction; - if (dropaction && dropaction !== "move") { - added = this.addDocument(docDragData.droppedDocuments); - } else if (docDragData.moveDocument) { + const dropAction = docDragData.dropAction || docDragData.userDropAction; + if ((!dropAction || dropAction === "move") && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); const res = addedDocs.length ? this.addDocument(addedDocs) : true; @@ -210,6 +207,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); } else added = res; } else { + ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); } added && e.stopPropagation(); @@ -305,7 +303,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const reg = new RegExp(Utils.prepend(""), "g"); const modHtml = srcUrl ? html.replace(reg, srcUrl) : html; const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: "-web page-", _width: 300, _height: 300 }); - Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc)["text"] = text; + Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc).text = text; this.props.addDocument(htmlDoc); if (srcWeb) { const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")[0].contentDocument?.getSelection()?.focusNode as any); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 705871a6f..3c7471d7c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -90,7 +90,10 @@ class TreeView extends React.Component<TreeViewProps> { get displayName() { return "TreeView(" + this.doc.title + ")"; } // this makes mobx trace() statements more descriptive get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; } + set treeViewOpen(c: boolean) { + if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; + else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; + } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } @@ -101,7 +104,7 @@ class TreeView extends React.Component<TreeViewProps> { const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields - DocListCast(this.doc[field])) as Doc[]; // otherwise use the document's data field + DocListCast(this.doc[field])); // otherwise use the document's data field } @computed get childDocs() { return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } @@ -328,7 +331,7 @@ class TreeView extends React.Component<TreeViewProps> { [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} </ul >; } else if (this.treeViewExpandedView === "fields") { - return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.doc[Id] + this.doc.title}> + return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > {this.expandedField} </div></ul>; } else { @@ -782,7 +785,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll onClicks.push({ description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index b630f9cf8..a5aef86de 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -24,6 +24,7 @@ border-right: unset; z-index: 2; } + .collectionTimeView-treeView { display: flex; flex-direction: column; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index d41248a77..123519b08 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util'; import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -78,6 +78,7 @@ export interface CollectionViewCustomProps { childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection childLayoutString?: string; // specify a layout string to use for children of the collection childOpacity?: () => number; + hideFilter?: true; } export interface CollectionRenderProps { @@ -143,20 +144,20 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const effectiveAcl = GetEffectiveAcl(this.props.Document); if (added.length) { - if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { + if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { return false; } else { - if (this.props.Document[AclSym]) { - // change so it only adds if more restrictive - added.forEach(d => { - // const dataDoc = d[DataSym]; - for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); - } - // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; - }); - } + // if (this.props.Document[AclSym]) { + // // change so it only adds if more restrictive + // added.forEach(d => { + // // const dataDoc = d[DataSym]; + // for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + // // key.substring(4).replace("_", ".") !== Doc.CurrentUserEmail && distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + // distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + // } + // }); + // } if (effectiveAcl === AclAddonly) { added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); @@ -180,7 +181,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus doc.context = this.props.Document; }); added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); + // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); + (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); targetDataDoc["lastModified"] = new DateField(new Date(Date.now())); @@ -192,14 +194,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { - const effectiveAcl = GetEffectiveAcl(this.props.Document); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) { + const collectionEffectiveAcl = GetEffectiveAcl(this.props.Document); + const docEffectiveAcl = GetEffectiveAcl(doc); + // you can remove the document if you either have Admin/Edit access to the collection or to the specific document + if (collectionEffectiveAcl === AclEdit || collectionEffectiveAcl === AclAdmin || docEffectiveAcl === AclAdmin || docEffectiveAcl === AclEdit) { const docs = doc instanceof Doc ? [doc] : doc as Doc[]; const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); - const result = value.filter(v => !docs.includes(v)); - if (result.length !== value.length) { - targetDataDoc[this.props.fieldKey] = new List<Doc>(result); + const toRemove = value.filter(v => docs.includes(v)); + if (toRemove.length !== 0) { + toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc)); return true; } } @@ -309,7 +313,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const options = cm.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; - optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + !Doc.UserDoc().noviceMode ? optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }) : null; if (this.props.Document.childLayout instanceof Doc) { optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); } @@ -338,7 +342,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus icon: "edit", event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), })); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); + !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); if (!Doc.UserDoc().noviceMode) { const more = cm.findByDescription("More..."); @@ -368,7 +372,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus get _facetWidth() { return NumCast(this.props.Document._facetWidth); } set _facetWidth(value) { this.props.Document._facetWidth = value; } - bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth(); + bodyPanelWidth = () => this.props.PanelWidth(); facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); @computed get dataDoc() { @@ -490,6 +494,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return false; }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0), false); } + filterBackground = () => "rgba(105, 105, 105, 0.432)"; get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) @computed get scriptField() { @@ -560,6 +565,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </div> </div>; } + childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString); @@ -590,11 +596,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href : ""))} - {(!this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : + {(Doc.UserDoc()?.noviceMode || !this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : <div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown} style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "55%" }} /> } - {this.facetWidth() < 10 ? (null) : this.filterView} + {Doc.UserDoc()?.noviceMode || this.facetWidth() < 10 ? (null) : this.filterView} </div>); } } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index 4e704b58f..bc9cf4848 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -2,11 +2,13 @@ div { overflow: visible !important; } + .metadataEntry-outerDiv { overflow: hidden !important; pointer-events: all; } } + .parentDocumentSelector-flyout { position: relative; z-index: 9999; @@ -31,26 +33,31 @@ border-left: 0px; } } + .parentDocumentSelector-button { - pointer-events: all; + pointer-events: all; position: relative; display: inline-block; + svg { - width:20px !important; - height:20px; + // width:20px !important; + //height:20px; } } + .parentDocumentSelector-metadata { pointer-events: auto; padding-right: 5px; width: 25px; display: inline-block; } + .buttonSelector { div { overflow: visible !important; } - display: inline-block; + + display: inline-block; width:100%; height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 8c0b8de9d..4c8cac3ed 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -42,14 +42,14 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { async fetchDocuments() { const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)); const containerProtoSets = await Promise.all(aliases.map(async alias => - await Promise.all((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); + ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => { - return (await SearchUtil.GetAliasesOfDocument(container)); + return (SearchUtil.GetAliasesOfDocument(container)); })); const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => { - return (await SearchUtil.GetAliasesOfDocument(dp)); + return (SearchUtil.GetAliasesOfDocument(dp)); })); const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys()); runInAction(() => { @@ -129,7 +129,7 @@ export class DockingViewButtonSelector extends React.Component<{ views: () => Do this.props.views()[0]?.select(false); }} className="buttonSelector"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> - <FontAwesomeIcon icon={"cog"} size={"sm"} /> + <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> </Flyout> </span>; } diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 829db9c6a..5c199096e 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -148,7 +148,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown; + changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 57336131a..badbc48a1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1144,7 +1144,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action componentDidMount() { super.componentDidMount?.(); - this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation }, + this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); @@ -1239,13 +1239,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); + !Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const viewctrls = ContextMenu.Instance.findByDescription("UI Controls..."); const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : []; - viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); - viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + + + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); @@ -1290,7 +1292,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P setTimeout(() => { SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { docs.docs.forEach(d => LinkManager.Instance.addLink(d)); - }) + }); }, 2000); // need to give solr some time to update so that this query will find any link docs we've added. } } diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss index 010beb836..d49ab27fb 100644 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -27,13 +27,15 @@ position: absolute; } -.sketch-picker { - background: #323232; - width: 160px !important; - height: 80% !important; - - .flexbox-fit { +.btn-group-palette { + .sketch-picker { background: #323232; + width: 160px !important; + height: 80% !important; + + .flexbox-fit { + background: #323232; + } } } diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx index ddc282e57..6263be261 100644 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx @@ -13,7 +13,6 @@ import AntimodeMenu from "../../AntimodeMenu"; import "./FormatShapePane.scss"; import { undoBatch } from "../../../util/UndoManager"; import { ColorState, SketchPicker } from 'react-color'; -import { DocumentView } from "../../../views/nodes/DocumentView" @observer export default class FormatShapePane extends AntimodeMenu { @@ -124,12 +123,12 @@ export default class FormatShapePane extends AntimodeMenu { console.log(ink); if (ink) { const newPoints: { X: number, Y: number }[] = []; - for (var j = 0; j < ink.length; j++) { + ink.forEach(i => { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth; - const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / oldHeight; + const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; + const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; newPoints.push({ X: newX, Y: newY }); - } + }); doc.data = new InkField(newPoints); } } @@ -148,12 +147,12 @@ export default class FormatShapePane extends AntimodeMenu { console.log(ink); if (ink) { const newPoints: { X: number, Y: number }[] = []; - for (var j = 0; j < ink.length; j++) { + ink.forEach(i => { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth; - const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / oldHeight; + const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; + const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; newPoints.push({ X: newX, Y: newY }); - } + }); doc.data = new InkField(newPoints); } } @@ -191,11 +190,11 @@ export default class FormatShapePane extends AntimodeMenu { if (ink) { const newPoints: { X: number, Y: number }[] = []; - for (var i = 0; i < ink.length; i++) { - const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X; - const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].Y; + ink.forEach(i => { + const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; + const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y; newPoints.push({ X: newX, Y: newY }); - } + }); doc.data = new InkField(newPoints); const xs = newPoints.map(p => p.X); const ys = newPoints.map(p => p.Y); @@ -395,12 +394,12 @@ export default class FormatShapePane extends AntimodeMenu { @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); } @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } - @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); } @computed get controlPoints() { return this.controlPointsButton(); } @computed get lockRatio() { return this.lockRatioButton(); } @computed get rotate90() { return this.rotate90Button(); } + @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } @computed get propertyGroupItems() { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 764758eee..a32c8b363 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; -import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util"; +import { GetEffectiveAcl } from "../../../../fields/util"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downX = x; this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document); - if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); this.clearSelection(); } }); diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss new file mode 100644 index 000000000..7df56115f --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -0,0 +1,644 @@ +.propertiesView { + + background-color: rgb(205, 205, 205); + height: 100%; + font-family: "Noto Sans"; + cursor: auto; + + overflow-x: visible; + overflow-y: visible; + + .propertiesView-title { + background-color: rgb(159, 159, 159); + text-align: center; + padding-top: 12px; + padding-bottom: 12px; + display: flex; + font-size: 18px; + font-weight: bold; + justify-content: center; + + .propertiesView-title-icon { + width: 20px; + height: 20px; + padding-left: 38px; + margin-top: -5px; + right: 19; + position: absolute; + + &:hover { + color: grey; + cursor: pointer; + } + + } + + } + + .propertiesView-name { + border-bottom: 1px solid black; + padding: 8.5px; + font-size: 12.5px; + + &:hover { + cursor: pointer; + } + } + + .propertiesView-settings { + border-bottom: 1px solid black; + //padding: 8.5px; + font-size: 12.5px; + font-weight: bold; + + .propertiesView-settings-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-settings-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-settings-content { + margin-left: 12px; + padding-bottom: 10px; + padding-top: 8px; + } + + } + + .propertiesView-sharing { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-sharing-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-sharing-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-sharing-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + } + } + + .propertiesView-appearance { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-appearance-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-appearance-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-appearance-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + } + } + + .propertiesView-transform { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-transform-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-transform-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-transform-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + } + } + + .notify-button { + padding: 2px; + width: 12px; + height: 12px; + background-color: black; + border-radius: 10px; + padding-left: 2px; + padding-right: 2px; + margin-top: 2px; + margin-left: 3px; + + .notify-button-icon { + width: 6px; + height: 6.5px; + margin-left: .5px; + } + + &:hover { + background-color: rgb(158, 158, 158); + cursor: pointer; + } + } + + .expansion-button-icon { + width: 11px; + height: 11px; + color: black; + margin-left: 27px; + + &:hover { + color: rgb(131, 131, 131); + cursor: pointer; + } + } + + .propertiesView-sharingTable { + + border: 1px solid black; + padding: 5px; + border-radius: 6px; + /* width: 170px; */ + margin-right: 10px; + background-color: #ececec; + max-height: 130px; + overflow-y: scroll; + + .propertiesView-sharingTable-item { + + display: flex; + padding: 3px; + align-items: center; + border-bottom: 0.5px solid grey; + + &:hover .propertiesView-sharingTable-item-name { + overflow-x: unset; + white-space: unset; + overflow-wrap: break-word; + } + + .propertiesView-sharingTable-item-name { + font-weight: bold; + width: 95px; + overflow-x: hidden; + display: inline-block; + text-overflow: ellipsis; + white-space: nowrap; + } + + .propertiesView-sharingTable-item-permission { + display: flex; + right: 34; + float: right; + position: absolute; + + .permissions-select { + z-index: 1; + border: none; + background-color: inherit; + width: 75px; + //text-align: justify; // for Edge + //text-align-last: end; + + &:hover { + cursor: pointer; + } + } + } + + &:last-child { + border-bottom: none; + } + } + } + + .propertiesView-fields { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-fields-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-fields-title-name { + font-size: 12.5px; + font-weight: bold; + white-space: nowrap; + width: 35px; + display: flex; + } + + .propertiesView-fields-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-fields-checkbox { + float: right; + height: 20px; + margin-top: -9px; + + .propertiesView-fields-checkbox-text { + font-size: 7px; + margin-top: -10px; + margin-left: 6px; + } + } + + .propertiesView-fields-content { + font-size: 10px; + margin-left: 2px; + padding: 10px; + + &:hover { + cursor: pointer; + } + } + } + + .field { + display: flex; + font-size: 7px; + background-color: #e8e8e8; + padding-right: 3px; + border: 0.5px solid grey; + border-radius: 5px; + padding-left: 3px; + } + + .uneditable-field { + display: flex; + overflow-y: visible; + margin-bottom: 2px; + + &:hover { + cursor: auto; + } + } + + .propertiesView-layout { + + .propertiesView-layout-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-layout-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-layout-content { + overflow: hidden; + padding: 10px; + } + + } +} + +.inking-button { + + display: flex; + + .inking-button-points { + background-color: #333333; + padding: 7px; + border-radius: 7px; + margin-right: 32px; + width: 32; + height: 32; + padding-top: 9px; + margin-left: 18px; + + &:hover { + background: rgb(131, 131, 131); + transform: scale(1.05); + cursor: pointer; + } + } + + .inking-button-lock { + background-color: #333333; + padding: 7px; + border-radius: 7px; + margin-right: 32px; + width: 32; + height: 32; + padding-top: 9px; + padding-left: 10px; + + &:hover { + background: rgb(131, 131, 131); + transform: scale(1.05); + cursor: pointer; + } + } + + .inking-button-rotate { + background-color: #333333; + padding: 7px; + border-radius: 7px; + width: 32; + height: 32; + padding-top: 9px; + padding-left: 10px; + + &:hover { + background: rgb(131, 131, 131); + transform: scale(1.05); + cursor: pointer; + } + } +} + +.inputBox-duo { + display: flex; +} + +.inputBox { + + margin-top: 10px; + display: flex; + height: 19px; + margin-right: 15px; + + .inputBox-title { + font-size: 12px; + padding-right: 5px; + } + + .inputBox-input { + font-size: 10px; + width: 50px; + margin-right: 1px; + border-radius: 3px; + + &:hover { + cursor: pointer; + } + } + + .inputBox-button { + + .inputBox-button-up { + background-color: #333333; + height: 9px; + padding-left: 3px; + padding-right: 3px; + padding-top: 1px; + padding-bottom: 1px; + border-radius: 1.5px; + + &:hover { + background: rgb(131, 131, 131); + transform: scale(1.05); + cursor: pointer; + } + } + + .inputBox-button-down { + background-color: #333333; + height: 9px; + padding-left: 3px; + padding-right: 3px; + padding-top: 1px; + padding-bottom: 1px; + border-radius: 1.5px; + + &:hover { + background: rgb(131, 131, 131); + transform: scale(1.05); + cursor: pointer; + } + } + + } +} + +.color-palette { + width: 160px; + height: 360; +} + +.strokeAndFill { + display: flex; + margin-top: 10px; + + .fill { + margin-right: 40px; + display: flex; + padding-bottom: 7px; + margin-left: 35px; + + .fill-title { + font-size: 12px; + margin-right: 2px; + } + + .fill-button { + padding-top: 2px; + margin-top: -1px; + } + } + + .stroke { + display: flex; + + .stroke-title { + font-size: 12px; + } + + .stroke-button { + padding-top: 2px; + margin-left: 2px; + margin-top: -1px; + } + } +} + +.widthAndDash { + + .width { + .width-top { + display: flex; + + .width-title { + font-size: 12; + margin-right: 20px; + margin-left: 35px; + text-align: center; + } + + .width-input { + margin-right: 30px; + margin-top: -10px; + } + } + + .width-range { + margin-right: 1px; + margin-bottom: 6; + } + } + + .arrows { + + display: flex; + margin-bottom: 3px; + + .arrows-head { + + display: flex; + margin-right: 35px; + + .arrows-head-title { + font-size: 10; + } + + .arrows-head-input { + margin-left: 6px; + margin-top: 2px; + } + } + + .arrows-tail { + display: flex; + + .arrows-tail-title { + font-size: 10; + } + + .arrows-tail-input { + margin-left: 6px; + margin-top: 2px; + } + } + } + + .dashed { + + display: flex; + margin-left: 74px; + margin-bottom: 6px; + + .dashed-title { + font-size: 10; + } + + .dashed-input { + margin-left: 6px; + margin-top: 2px; + } + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx new file mode 100644 index 000000000..f5e0cd077 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -0,0 +1,850 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import "./PropertiesView.scss"; +import { observable, action, computed, runInAction } from "mobx"; +import { Doc, Field, DocListCast, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt } from "../../../../fields/Doc"; +import { DocumentView } from "../../nodes/DocumentView"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { EditableView } from "../../EditableView"; +import { KeyValueBox } from "../../nodes/KeyValueBox"; +import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { listSpec } from "../../../../fields/Schema"; +import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView"; +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"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip, Checkbox, Divider } from "@material-ui/core"; +import SharingManager from "../../../util/SharingManager"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import FormatShapePane from "./FormatShapePane"; +import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util"; +import { InkField } from "../../../../fields/InkField"; +import { undoBatch } from "../../../util/UndoManager"; +import { ColorState, SketchPicker } from "react-color"; +import AntimodeMenu from "../../AntimodeMenu"; +import "./FormatShapePane.scss"; +import { discovery_v1 } from "googleapis"; + + +interface PropertiesViewProps { + width: number; + height: number; + renderDepth: number; + ScreenToLocalTransform: () => Transform; + onDown: (event: any) => void; +} + +@observer +export class PropertiesView extends React.Component<PropertiesViewProps> { + + @computed get MAX_EMBED_HEIGHT() { return 200; } + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } + + @observable layoutFields: boolean = false; + + @observable openActions: boolean = true; + @observable openSharing: boolean = true; + @observable openFields: boolean = true; + @observable openLayout: boolean = true; + @observable openAppearance: boolean = true; + @observable openTransform: boolean = true; + + @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } + + @action + rtfWidth = () => { + if (this.selectedDoc) { + return Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20); + } else { + return 0; + } + } + @action + rtfHeight = () => { + if (this.selectedDoc) { + return this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; + } else { + return 0; + } + } + + @action + docWidth = () => { + if (this.selectedDoc) { + const layoutDoc = this.selectedDoc; + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.width - 20)); + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; + } else { + return 0; + } + } + + @action + docHeight = () => { + if (this.selectedDoc && this.dataDoc) { + const layoutDoc = this.selectedDoc; + return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return this.docWidth() * aspect; + return layoutDoc._fitWidth ? (!this.dataDoc._nativeHeight ? NumCast(this.props.height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, + NumCast(this.props.height)))) : + NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; + })())); + } else { + return 0; + } + } + + @computed get expandedField() { + if (this.dataDoc && this.selectedDoc) { + const ids: { [key: string]: string } = {}; + const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; + doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const rows: JSX.Element[] = []; + for (const key of Object.keys(ids).slice().sort()) { + const contents = doc[key]; + if (key[0] === "#") { + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> + + </div>); + } else { + let contentElement: (JSX.Element | null)[] | JSX.Element = []; + contentElement = <EditableView key="editableView" + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} + height={13} + fontSize={10} + GetValue={() => Field.toKeyValueString(doc, key)} + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + />; + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> + + {contentElement} + </div>); + } + } + rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> + <EditableView + key="editableView" + contents={"add key:value or #tags"} + height={13} + fontSize={10} + GetValue={() => ""} + SetValue={this.setKeyValue} /> + </div>); + return rows; + } + } + + @computed get noviceFields() { + if (this.dataDoc && this.selectedDoc) { + const ids: { [key: string]: string } = {}; + const doc = this.dataDoc; + doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const rows: JSX.Element[] = []; + for (const key of Object.keys(ids).slice().sort()) { + if ((key[0] === key[0].toUpperCase() && key.substring(0, 3) !== "ACL") + || key[0] === "#" || key === "author" || + key === "creationDate" || key.indexOf("lastModified") !== -1) { + + const contents = doc[key]; + if (key[0] === "#") { + rows.push(<div className="uneditable-field" key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> + + </div>); + } else { + const value = Field.toString(contents as Field); + if (key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { + rows.push(<div className="uneditable-field" key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> + <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> + </div>); + } else { + let contentElement: (JSX.Element | null)[] | JSX.Element = []; + contentElement = <EditableView key="editableView" + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} + height={13} + fontSize={10} + GetValue={() => Field.toKeyValueString(doc, key)} + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + />; + + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> + + {contentElement} + </div>); + } + } + } + } + rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> + <EditableView + key="editableView" + contents={"add key:value or #tags"} + height={13} + fontSize={10} + GetValue={() => ""} + SetValue={this.setKeyValue} /> + </div>); + return rows; + } + } + + @undoBatch + setKeyValue = (value: string) => { + if (this.selectedDoc && this.dataDoc) { + const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; + if (value.indexOf(":") !== -1) { + const newVal = value[0].toUpperCase() + value.substring(1, value.length); + KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + return true; + } else if (value[0] === "#") { + const newVal = value + `:'${value}'`; + KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + return true; + } + } + return false; + } + + @computed get layoutPreview() { + if (this.selectedDoc) { + const layoutDoc = Doc.Layout(this.selectedDoc); + const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; + const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; + return <div style={{ display: "inline-block", height: panelHeight() }} key={this.selectedDoc[Id]}> + <ContentFittingDocumentView + Document={layoutDoc} + DataDoc={this.dataDoc} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnFalse} + treeViewDoc={undefined} + backgroundColor={() => "lightgrey"} + fitToBox={false} + FreezeDimensions={true} + NativeWidth={layoutDoc.type === + StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : returnZero} + NativeHeight={layoutDoc.type === + StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : returnZero} + PanelWidth={panelWidth} + PanelHeight={panelHeight} + focus={returnFalse} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + docFilters={returnEmptyFilter} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + addDocument={returnFalse} + moveDocument={undefined} + removeDocument={returnFalse} + parentActive={() => false} + whenActiveChanged={emptyFunction} + addDocTab={returnFalse} + pinToPres={emptyFunction} + bringToFront={returnFalse} + ContentScaling={returnOne} + dontRegisterView={true} + /> + </div>; + } else { + return null; + } + } + + @undoBatch + changePermissions = (e: any, user: string) => { + SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!); + } + + getPermissionsSelect(user: string) { + return <select className="permissions-select" + onChange={e => this.changePermissions(e, user)}> + {Object.values(SharingPermissions).map(permission => { + return ( + <option key={permission} value={permission} selected={this.selectedDoc![`ACL-${user.replace(".", "_")}`] === permission}> + {permission} + </option>); + })} + </select>; + } + + @computed get notifyIcon() { + return <Tooltip title={<><div className="dash-tooltip">Notify with message</div></>}> + <div className="notify-button"> + <FontAwesomeIcon className="notify-button-icon" icon="bell" color="white" size="sm" /> + </div> + </Tooltip>; + } + + @computed get expansionIcon() { + return <Tooltip title={<><div className="dash-tooltip">{"Show more permissions"}</div></>}> + <div className="expansion-button" onPointerDown={() => { + if (this.selectedDocumentView) { + SharingManager.Instance.open(this.selectedDocumentView); + } + }}> + <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> + </div> + </Tooltip>; + } + + sharingItem(name: string, effectiveAcl: symbol, permission?: string) { + return <div className="propertiesView-sharingTable-item"> + <div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div> + {/* {name !== "Me" ? this.notifyIcon : null} */} + <div className="propertiesView-sharingTable-item-permission"> + {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name) : permission} + {permission === "Owner" ? this.expansionIcon : null} + </div> + </div>; + } + + @computed get sharingTable() { + const AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] + ]); + + const effectiveAcl = GetEffectiveAcl(this.selectedDoc!); + const tableEntries = []; + + if (this.selectedDoc![AclSym]) { + for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) { + const name = key.substring(4).replace("_", "."); + if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author) tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); + } + } + + tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]))); + if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.author), effectiveAcl, "Owner")); + + return <div className="propertiesView-sharingTable"> + {tableEntries} + </div>; + } + + @computed get fieldsCheckbox() { + return <Checkbox + color="primary" + onChange={this.toggleCheckbox} + checked={this.layoutFields} + />; + } + + @undoBatch + @action + toggleCheckbox = () => { + this.layoutFields = !this.layoutFields; + } + + @computed get editableTitle() { + return <EditableView + key="editableView" + contents={StrCast(this.selectedDoc?.title)} + height={25} + fontSize={14} + GetValue={() => StrCast(this.selectedDoc?.title)} + SetValue={this.setTitle} />; + } + + @undoBatch + @action + setTitle = (value: string) => { + if (this.dataDoc) { + this.selectedDoc && (this.selectedDoc.title = value); + KeyValueBox.SetField(this.dataDoc, "title", value, true); + return true; + } + return false; + } + + + @undoBatch + @action + rotate = (angle: number) => { + const _centerPoints: { X: number, Y: number }[] = []; + if (this.selectedDoc) { + const doc = this.selectedDoc; + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const xs = ink.map(p => p.X); + const ys = ink.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + _centerPoints.push({ X: left, Y: top }); + } + } + + var index = 0; + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + doc.rotation = Number(doc.rotation) + Number(angle); + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + + const newPoints: { X: number, Y: number }[] = []; + for (var i = 0; i < ink.length; i++) { + const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X; + const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].Y; + newPoints.push({ X: newX, Y: newY }); + } + doc.data = new InkField(newPoints); + const xs = newPoints.map(p => p.X); + const ys = newPoints.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + + doc._height = (bottom - top); + doc._width = (right - left); + } + index++; + } + } + } + + @observable _controlBtn: boolean = false; + @observable _lock: boolean = false; + + @computed + get controlPointsButton() { + return <div className="inking-button"> + <Tooltip title={<><div className="dash-tooltip">{"Edit points"}</div></>}> + <div className="inking-button-points" onPointerDown={action(() => this._controlBtn = !this._controlBtn)} style={{ backgroundColor: this._controlBtn ? "black" : "" }}> + <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> + </div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">{this._lock ? "Unlock points" : "Lock points"}</div></>}> + <div className="inking-button-lock" onPointerDown={action(() => this._lock = !this._lock)} > + <FontAwesomeIcon icon={this._lock ? "unlock" : "lock"} color="white" size="lg" /> + </div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Rotate 90˚"}</div></>}> + <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}> + <FontAwesomeIcon icon="undo" color="white" size="lg" /> + </div> + </Tooltip> + </div>; + } + + inputBox = (key: string, value: any, setter: (val: string) => {}, title: string) => { + return <div className="inputBox" + style={{ + marginRight: title === "X:" ? "19px" : "", + marginLeft: title === "∠:" ? "39px" : "" + }}> + <div className="inputBox-title"> {title} </div> + <input className="inputBox-input" + type="text" value={value} + onChange={e => setter(e.target.value)} /> + <div className="inputBox-button"> + <div className="inputBox-button-up" key="up2" + onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > + <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> + </div> + <div className="inputbox-Button-down" key="down2" + onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} > + <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> + </div> + </div> + </div>; + } + + inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { + return <div className="inputBox-duo"> + {this.inputBox(key, value, setter, title1)} + {title2 === "" ? (null) : this.inputBox(key2, value2, setter2, title2)} + </div>; + } + + @action + upDownButtons = (dirs: string, field: string) => { + switch (field) { + case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; + // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break; + case "Xps": this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === "up" ? 10 : -10)); break; + case "Yps": this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === "up" ? 10 : -10)); break; + case "stk": this.selectedDoc && (this.selectedDoc.strokeWidth = NumCast(this.selectedDoc?.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; + case "wid": + const oldWidth = NumCast(this.selectedDoc?._width); + const oldHeight = NumCast(this.selectedDoc?._height); + const oldX = NumCast(this.selectedDoc?.x); + const oldY = NumCast(this.selectedDoc?.y); + this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); + this._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); + const doc = this.selectedDoc; + if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { + console.log(doc.x, doc.y, doc._height, doc._width); + const ink = Cast(doc.data, InkField)?.inkData; + console.log(ink); + if (ink) { + const newPoints: { X: number, Y: number }[] = []; + for (var j = 0; j < ink.length; j++) { + // (new x — oldx) + (oldxpoint * newWidt)/oldWidth + const newX = (NumCast(doc.x) - oldX) + (ink[j].X * NumCast(doc._width)) / oldWidth; + const newY = (NumCast(doc.y) - oldY) + (ink[j].Y * NumCast(doc._height)) / oldHeight; + newPoints.push({ X: newX, Y: newY }); + } + doc.data = new InkField(newPoints); + } + } + break; + case "hgt": + const oWidth = NumCast(this.selectedDoc?._width); + const oHeight = NumCast(this.selectedDoc?._height); + const oX = NumCast(this.selectedDoc?.x); + const oY = NumCast(this.selectedDoc?.y); + this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); + this._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); + const docu = this.selectedDoc; + if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { + console.log(docu.x, docu.y, docu._height, docu._width); + const ink = Cast(docu.data, InkField)?.inkData; + console.log(ink); + if (ink) { + const newPoints: { X: number, Y: number }[] = []; + for (var j = 0; j < ink.length; j++) { + // (new x — oldx) + (oldxpoint * newWidt)/oldWidth + const newX = (NumCast(docu.x) - oX) + (ink[j].X * NumCast(docu._width)) / oWidth; + const newY = (NumCast(docu.y) - oY) + (ink[j].Y * NumCast(docu._height)) / oHeight; + newPoints.push({ X: newX, Y: newY }); + } + docu.data = new InkField(newPoints); + } + } + break; + } + } + + getField(key: string) { + //if (this.selectedDoc) { + return Field.toString(this.selectedDoc[key] as Field); + // } else { + // return undefined as Opt<string>; + // } + } + + @computed get shapeXps() { return this.getField("x"); } + @computed get shapeYps() { return this.getField("y"); } + @computed get shapeRot() { return this.getField("rotation"); } + @computed get shapeHgt() { return this.getField("_height"); } + @computed get shapeWid() { return this.getField("_width"); } + set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Number(value)); } + set shapeYps(value) { this.selectedDoc && (this.selectedDoc.y = Number(value)); } + set shapeRot(value) { this.selectedDoc && (this.selectedDoc.rotation = Number(value)); } + set shapeWid(value) { + const oldWidth = NumCast(this.selectedDoc?._width); + this.selectedDoc && (this.selectedDoc._width = Number(value)); + this._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); + } + set shapeHgt(value) { + const oldHeight = NumCast(this.selectedDoc?._height); + this.selectedDoc && (this.selectedDoc._height = Number(value)); + this._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); + } + + @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); } + @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } + @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } + + @observable private _fillBtn = false; + @observable private _lineBtn = false; + + private _lastFill = "#D0021B"; + private _lastLine = "#D0021B"; + private _lastDash: any = "2"; + + @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } + @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } + set colorFil(value) { value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } + set colorStk(value) { value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } + + colorButton(value: string, setter: () => {}) { + return <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> + <div className="color-button-preview" style={{ + backgroundColor: value ?? "121212", width: 15, height: 15, + display: value === "" || value === "transparent" ? "none" : "" + }} /> + {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -14, position: "fixed" }}>☒</p> : ""} + </div>; + } + + @undoBatch + @action + switchStk = (color: ColorState) => { + const val = String(color.hex); + this.colorStk = val; + return true; + } + @undoBatch + @action + switchFil = (color: ColorState) => { + const val = String(color.hex); + this.colorFil = val; + return true; + } + + colorPicker(setter: (color: string) => {}, type: string) { + return <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', 'transparent']} + color={type === "stk" ? this.colorStk : this.colorFil} />; + } + + @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } + @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } + + @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } + @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } + + @computed get strokeAndFill() { + return <div> + <div key="fill" className="strokeAndFill"> + <div className="fill"> + <div className="fill-title">Fill:</div> + <div className="fill-button">{this.fillButton}</div> + </div> + <div className="stroke"> + <div className="stroke-title"> Stroke: </div> + <div className="stroke-button">{this.lineButton}</div> + </div> + </div> + {this._fillBtn ? this.fillPicker : ""} + {this._lineBtn ? this.linePicker : ""} + </div>; + } + + @computed get solidStk() { return this.selectedDoc?.color && (!this.selectedDoc?.strokeDash || this.selectedDoc?.strokeDash === "0") ? true : false; } + @computed get dashdStk() { return this.selectedDoc?.strokeDash || ""; } + @computed get unStrokd() { return this.selectedDoc?.color ? true : false; } + @computed get widthStk() { return this.getField("strokeWidth") || "1"; } + @computed get markHead() { return this.getField("strokeStartMarker") || ""; } + @computed get markTail() { return this.getField("strokeEndMarker") || ""; } + set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } + set dashdStk(value) { + value && (this._lastDash = value) && (this.unStrokd = false); + this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined); + } + set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); } + set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } + set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); } + set markTail(value) { this.selectedDoc && (this.selectedDoc.strokeEndMarker = value); } + + + @computed get stkInput() { return this.regInput("stk", this.widthStk, (val: string) => this.widthStk = val); } + + + regInput = (key: string, value: any, setter: (val: string) => {}) => { + return <div className="inputBox"> + <input className="inputBox-input" + type="text" value={value} + onChange={e => setter(e.target.value)} /> + <div className="inputBox-button"> + <div className="inputBox-button-up" key="up2" + onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > + <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> + </div> + <div className="inputbox-Button-down" key="down2" + onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} > + <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> + </div> + </div> + </div>; + } + + @computed get widthAndDash() { + return <div className="widthAndDash"> + <div className="width"> + <div className="width-top"> + <div className="width-title">Width:</div> + <div className="width-input">{this.stkInput}</div> + </div> + <input className="width-range" type="range" + defaultValue={Number(this.widthStk)} min={1} max={100} + onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> + </div> + + <div className="arrows"> + <div className="arrows-head"> + <div className="arrows-head-title" >Arrow Head: </div> + <input key="markHead" className="arrows-head-input" type="checkbox" + checked={this.markHead !== ""} + onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} /> + </div> + <div className="arrows-tail"> + <div className="arrows-tail-title" >Arrow End: </div> + <input key="markTail" className="arrows-tail-input" type="checkbox" + checked={this.markTail !== ""} + onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} /> + </div> + </div> + <div className="dashed"> + <div className="dashed-title">Dash:</div> + <input key="markHead" className="dashed-input" + type="checkbox" checked={this.dashdStk === "2"} + onChange={this.changeDash} /> + </div> + </div>; + } + + @undoBatch @action + changeDash = () => { + this.dashdStk = this.dashdStk === "2" ? "0" : "2"; + } + + @computed get appearanceEditor() { + return <div className="appearance-editor"> + {this.widthAndDash} + {this.strokeAndFill} + </div>; + } + + @computed get transformEditor() { + return <div className="transform-editor"> + {this.controlPointsButton} + {this.hgtInput} + {this.XpsInput} + {this.rotInput} + </div>; + } + + render() { + + if (!this.selectedDoc) { + return <div className="propertiesView" style={{ width: this.props.width }}> + <div className="propertiesView-title" style={{ width: this.props.width }}> + No Document Selected + <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> + <FontAwesomeIcon icon="times" color="black" size="xs" /> + </div> + </div> + </div>; + } + + const novice = Doc.UserDoc().noviceMode; + + return <div className="propertiesView" style={{ width: this.props.width }} > + <div className="propertiesView-title" style={{ width: this.props.width }}> + Properties + <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> + <FontAwesomeIcon icon="times" color="black" size="sm" /> + </div> + </div> + <div className="propertiesView-name"> + {this.editableTitle} + </div> + <div className="propertiesView-settings"> + <div className="propertiesView-settings-title" + onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} + style={{ backgroundColor: this.openActions ? "black" : "" }}> + Actions + <div className="propertiesView-settings-title-icon"> + <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openActions ? (null) : + <div className="propertiesView-settings-content"> + <PropertiesButtons /> + </div>} + </div> + <div className="propertiesView-sharing"> + <div className="propertiesView-sharing-title" + onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} + style={{ backgroundColor: this.openSharing ? "black" : "" }}> + Sharing {"&"} Permissions + <div className="propertiesView-sharing-title-icon"> + <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openSharing ? (null) : + <div className="propertiesView-sharing-content"> + {this.sharingTable} + </div>} + </div> + + {!this.isInk ? (null) : + <div className="propertiesView-appearance"> + <div className="propertiesView-appearance-title" + onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} + style={{ backgroundColor: this.openAppearance ? "black" : "" }}> + Appearance + <div className="propertiesView-appearance-title-icon"> + <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openAppearance ? (null) : + <div className="propertiesView-appearance-content"> + {this.appearanceEditor} + </div>} + </div>} + + {this.isInk ? <div className="propertiesView-transform"> + <div className="propertiesView-transform-title" + onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} + style={{ backgroundColor: this.openTransform ? "black" : "" }}> + Transform + <div className="propertiesView-transform-title-icon"> + <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openTransform ? <div className="propertiesView-transform-content"> + {this.transformEditor} + </div> : null} + </div> : null} + + <div className="propertiesView-fields"> + <div className="propertiesView-fields-title" + onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} + style={{ backgroundColor: this.openFields ? "black" : "" }}> + <div className="propertiesView-fields-title-name"> + Fields {"&"} Tags + <div className="propertiesView-fields-title-icon"> + <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + </div> + {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> + {this.fieldsCheckbox} + <div className="propertiesView-fields-checkbox-text">Layout</div> + </div> : null} + {!this.openFields ? (null) : + <div className="propertiesView-fields-content"> + {novice ? this.noviceFields : this.expandedField} + </div>} + </div> + <div className="propertiesView-layout"> + <div className="propertiesView-layout-title" + onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} + style={{ backgroundColor: this.openLayout ? "black" : "" }}> + Layout + <div className="propertiesView-layout-title-icon" onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> + <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openLayout ? <div className="propertiesView-layout-content">{this.layoutPreview}</div> : null} + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 3e54d001b..4c79a7c2f 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -9,10 +9,10 @@ $main-accent: #aaaaa3; //$alt-accent: #59dff7; $alt-accent: #c2c2c5; $lighter-alt-accent: rgb(207, 220, 240); -$darker-alt-accent: rgb(178, 206, 248); +$darker-alt-accent: #b2cef8; $intermediate-color: #9c9396; $dark-color: #121721; -$link-color: lightBlue; +$link-color: #add8e6; $antimodemenu-height: 35px; // fonts $sans-serif: "Noto Sans", diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 04329182e..660afd4b9 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -1,20 +1,16 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, computed, toJS } from "mobx"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../fields/Doc"; -import { StrCast, DateCast } from "../../../fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { DateCast, StrCast } from "../../../fields/Types"; import { Utils } from "../../../Utils"; import { LinkManager } from "../../util/LinkManager"; +import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; import React = require("react"); -import { DocumentView } from "../nodes/DocumentView"; -import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; -import { EditableView } from "../EditableView"; -import { RefObject } from "react"; -import { Tooltip } from "@material-ui/core"; -import { undoBatch } from "../../util/UndoManager"; library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); @@ -296,21 +292,18 @@ export class LinkEditor extends React.Component<LinkEditorProps> { //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; - @undoBatch - @action + @undoBatch @action deleteLink = (): void => { LinkManager.Instance.deleteLink(this.props.linkDoc); this.props.showLinks(); } - @action + @undoBatch @action setDescripValue = (value: string) => { if (LinkManager.currentLink) { LinkManager.currentLink.description = value; this.buttonColor = "rgb(62, 133, 55)"; - setTimeout(action(() => { - this.buttonColor = "black"; - }), 750); + setTimeout(action(() => this.buttonColor = "black"), 750); return true; } } @@ -362,7 +355,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { this.openDropdown = !this.openDropdown; } - @action + @undoBatch @action changeFollowBehavior = (follow: string) => { this.openDropdown = false; Doc.GetProto(this.props.linkDoc).followLinkLocation = follow; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index d1c839c3b..6feb0a398 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -167,6 +167,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { DocumentLinksButton.EditLink = undefined; } + @undoBatch @action showLink = () => { this.props.linkDoc.hidden = !this.props.linkDoc.hidden; @@ -182,7 +183,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { switch (this.props.destinationDoc.type) { case DocumentType.IMG: destinationIcon = "image"; break; case DocumentType.COMPARISON: destinationIcon = "columns"; break; - case DocumentType.RTF: destinationIcon = "font"; break; + case DocumentType.RTF: destinationIcon = "sticky-note"; break; case DocumentType.COL: destinationIcon = "folder"; break; case DocumentType.WEB: destinationIcon = "globe-asia"; break; case DocumentType.SCREENSHOT: destinationIcon = "photo-video"; break; @@ -196,6 +197,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { case DocumentType.DOCHOLDER: destinationIcon = "expand"; break; case DocumentType.VID: destinationIcon = "video"; break; case DocumentType.INK: destinationIcon = "pen-nib"; break; + case DocumentType.PDF: destinationIcon = "file"; break; default: destinationIcon = "question"; break; } diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 3b91ac6b5..31efaaaa6 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -4,7 +4,7 @@ import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../fields/Doc"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; +import { emptyFunction, returnFalse, setupMoveUpEvents, emptyPath } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { LinkManager } from "../../util/LinkManager"; @@ -67,7 +67,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { if (doubleTap && this.props.InMenu && this.props.StartLink) { //action(() => Doc.BrushDoc(this.props.View.Document)); - DocumentLinksButton.StartLink = this.props.View; + if (DocumentLinksButton.StartLink === this.props.View) { + DocumentLinksButton.StartLink = undefined; + } else { + DocumentLinksButton.StartLink = this.props.View; + } } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; @@ -78,7 +82,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @action @undoBatch onLinkClick = (e: React.MouseEvent): void => { if (this.props.InMenu && this.props.StartLink) { - DocumentLinksButton.StartLink = this.props.View; + if (DocumentLinksButton.StartLink === this.props.View) { + DocumentLinksButton.StartLink = undefined; + } else { + DocumentLinksButton.StartLink = this.props.View; + } + //action(() => Doc.BrushDoc(this.props.View.Document)); } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; @@ -86,77 +95,65 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } } - @action @undoBatch completeLink = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => { - if (doubleTap && this.props.InMenu && !!!this.props.StartLink) { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action((e, doubleTap) => { + if (doubleTap && this.props.InMenu && !this.props.StartLink) { if (DocumentLinksButton.StartLink === this.props.View) { DocumentLinksButton.StartLink = undefined; - } else { - - if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { - const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); - LinkManager.currentLink = linkDoc; - - runInAction(() => { - if (linkDoc) { - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = e.screenX; - TaskCompletionBox.popupY = e.screenY - 133; - TaskCompletionBox.taskCompleted = true; - - LinkDescriptionPopup.popupX = e.screenX; - LinkDescriptionPopup.popupY = e.screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + LinkManager.currentLink = linkDoc; + if (linkDoc) { + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = e.screenX; + TaskCompletionBox.popupY = e.screenY - 133; + TaskCompletionBox.taskCompleted = true; - setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); - } + LinkDescriptionPopup.popupX = e.screenX; + LinkDescriptionPopup.popupY = e.screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; - }); + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); } } } - })); + }))); } - - @action @undoBatch - finishLinkClick = (screenX: number, screenY: number) => { + finishLinkClick = undoBatch(action((screenX: number, screenY: number) => { if (DocumentLinksButton.StartLink === this.props.View) { DocumentLinksButton.StartLink = undefined; - } else { - if (this.props.InMenu && !!!this.props.StartLink) { - if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { - const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); - // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved - runInAction(() => DocumentLinksButton.StartLink!._link = this.props.View._link = linkDoc); - setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); - LinkManager.currentLink = linkDoc; - - runInAction(() => { - if (linkDoc) { - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = screenX; - TaskCompletionBox.popupY = screenY - 133; - TaskCompletionBox.taskCompleted = true; - - if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { - LinkDescriptionPopup.popupX = screenX; - LinkDescriptionPopup.popupY = screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; - } - - setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); - } - }); + } else if (this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved + DocumentLinksButton.StartLink!._link = this.props.View._link = linkDoc; + setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); + LinkManager.currentLink = linkDoc; + if (linkDoc) { + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = screenX; + TaskCompletionBox.popupY = screenY - 133; + TaskCompletionBox.taskCompleted = true; + + if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = screenX; + LinkDescriptionPopup.popupY = screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; } + + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); } } - } + })); @observable public static EditLink: DocumentView | undefined; public static EditLinkLoc: number[] = [0, 0]; + + @action clearLinks() { + DocumentLinksButton.StartLink = undefined; + } + @computed get linkButton() { TraceMobx(); @@ -198,22 +195,33 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp // }))} > + {/* {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : + <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} */} + {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : - <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} + link : links.length} </div> - {DocumentLinksButton.StartLink && this.props.InMenu && !!!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} - style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} - onPointerDown={this.completeLink} onClick={e => this.finishLinkClick(e.screenX, e.screenY)} /> : (null)} + {this.props.InMenu && !this.props.StartLink && + DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} + style={{ + width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", + backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", + border: DocumentLinksButton.StartLink ? "" : "none" + }} + onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction} + onClick={e => DocumentLinksButton.StartLink ? this.finishLinkClick(e.screenX, e.screenY) : emptyFunction} /> : (null)} {DocumentLinksButton.StartLink === this.props.View && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} - style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} /> : (null)} + style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} + onPointerDown={this.clearLinks} onClick={this.clearLinks} + /> : (null)} </div>; return (!links.length) && !this.props.AlwaysOn ? (null) : - this.props.InMenu ? + this.props.InMenu && (this.props.StartLink || DocumentLinksButton.StartLink) ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> {linkButton} - </Tooltip> : !!!DocumentLinksButton.EditLink ? + </Tooltip> : !!!DocumentLinksButton.EditLink && !this.props.InMenu ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> {linkButton} </Tooltip> : diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b978f6245..e6b8928d4 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -54,6 +54,15 @@ } } + .documentView-anchorCont { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: inline-block; + } + .documentView-lock { width: 20; height: 20; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 15cf9556b..a195f2813 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -94,6 +94,8 @@ export interface DocumentViewProps { layoutKey?: string; radialMenu?: String[]; display?: string; + relative?: boolean; + scriptContext?: any; } @observer @@ -287,7 +289,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && + if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { let stopPropagate = true; let preventDefault = true; @@ -319,10 +321,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, + scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log); - if (this.props.Document !== Doc.UserDoc()["dockedBtn-undo"] && this.props.Document !== Doc.UserDoc()["dockedBtn-redo"]) { + if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc)) { UndoManager.RunInBatch(func, "on click"); } else func(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself @@ -584,6 +587,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + + @undoBatch + noOnClick = (): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = false; + } + + @undoBatch + toggleDetail = (): void => { + this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`); + } + @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (this.props.Document === Doc.UserDoc().activeWorkspace) { @@ -661,6 +676,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action + onCopy = () => { + const alias = Doc.MakeAlias(this.props.Document); + alias.x = NumCast(this.props.Document.x) + NumCast(this.props.Document._width); + alias.y = NumCast(this.props.Document.y) + 30; + this.props.addDocument?.(alias); + } + + @action onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (!(e instanceof Touch)) { @@ -704,14 +727,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); 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.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "concierge-bell" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("onRight", false, false), icon: "concierge-bell" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "concierge-bell" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "snowflake" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "hand-point-right" }); + onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); + !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("onRight", false, false), icon: "link" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); + onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); + !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); const funcs: ContextMenuProps[] = []; if (this.layoutDoc.onDragStart) { @@ -724,6 +747,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems = more && "subitems" in more ? more.subitems : []; moreItems.push({ description: "Download document", icon: "download", event: async () => Doc.Zip(this.props.Document) }); + moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "users" }); + //moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + moreItems.push({ description: "Create an Alias", event: () => this.onCopy(), icon: "copy" }); if (!Doc.UserDoc().noviceMode) { moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); @@ -735,16 +761,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } + //GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + const effectiveAcl = GetEffectiveAcl(this.props.Document); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); const help = cm.findByDescription("Help..."); const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" }); - helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); @@ -786,8 +814,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed.struct get linkOffset() { return [-15, 0]; } @computed get contents() { + const pos = this.props.relative ? "relative " : "absolute"; TraceMobx(); - return (<div style={{ position: "absolute", width: "100%", height: "100%" }}> + return (<div style={{ width: "100%", height: "100%" }}> <DocumentContentsView key={1} docFilters={this.props.docFilters} ContainingCollectionView={this.props.ContainingCollectionView} @@ -823,6 +852,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} + scriptContext={this.props.scriptContext} onClick={this.onClickFunc} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} @@ -860,7 +890,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView ? (null) : // view that are not registered DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => - <DocumentView {...this.props} key={i + 1} + <div className="documentView-anchorCont" key={i + 1}> <DocumentView {...this.props} Document={d} ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox @@ -873,12 +903,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu pointerEvents={false} LayoutTemplate={undefined} LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} - />); + /></div >); } @computed get innards() { TraceMobx(); + const pos = this.props.relative ? "relative" : undefined; if (this.props.treeViewDoc && !this.props.LayoutTemplateString?.includes("LinkAnchorBox")) { // this happens when the document is a tree view label (but not an anchor dot) - return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}> + return <div className="documentView-treeView" style={{ + maxWidth: this.props.PanelWidth() || undefined, + position: pos + }}> {StrCast(this.props.Document.title)} {this.allAnchors} </div>; @@ -1004,7 +1038,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu background: finalColor, opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), - fontSize: Cast(this.Document._fontSize, "string", null) + fontSize: Cast(this.Document._fontSize, "string", null), }}> {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <> {this.innards} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c10ff22fe..2cbfcdb0b 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -67,6 +67,7 @@ export interface FieldViewProps { color?: string; xMargin?: number; yMargin?: number; + scriptContext?: any; } @observer diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 5b85d8b0b..5bdafd857 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -1,25 +1,62 @@ -.fontIconBox-outerDiv { +.fontIconBox-label { + color: white; + margin-right: 4px; + margin-top: 1px; + position: relative; + text-align: center; + font-size: 7px; + letter-spacing: normal; + background-color: inherit; + border-radius: 8px; + margin-top: -8px; + padding: 0; + width: 100%; +} + +.menuButton-round { + border-radius: 100%; + + .fontIconBox-label { + margin-left: -10px; // button padding is 10px; + bottom: 0; + position: absolute; + } +} + +.menuButton-square { + padding-top: 3px; + padding-bottom: 3px; + padding-left: 5px; + + .fontIconBox-label { + border-radius: 0px; + margin-top: 0px; + border-radius: "inherit"; + } +} + +.menuButton, +.menuButton-round, +.menuButton-square { width: 100%; height: 100%; pointer-events: all; touch-action: none; - border-radius: inherit; - background: black; - border-radius: 100%; - transform-origin: top left; - .fontIconBox-label { - background: gray; - color:white; + .menuButton-wrap { + touch-action: none; border-radius: 8px; - width:100%; - position: absolute; - text-align: center; - font-size: 8px; - margin-top:4px; - letter-spacing: normal; - left: 0; - overflow: hidden; + + // &:hover { + // background: rgb(61, 61, 61); + // cursor: pointer; + // } + } + + .menuButton-icon-square { + width: auto; + height: 32px; + padding: 4px; } svg { diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 2611d2ca7..eff5a4160 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -6,14 +6,14 @@ import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import { StrCast, Cast, NumCast } from '../../../fields/Types'; -import { Utils } from "../../../Utils"; +import { Utils, emptyFunction } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; import { Doc } from '../../../fields/Doc'; import { ContextMenu } from '../ContextMenu'; import { ScriptField } from '../../../fields/ScriptField'; import { Tooltip } from '@material-ui/core'; const FontIconSchema = createSchema({ - icon: "string" + icon: "string", }); type FontIconDocument = makeInterface<[typeof FontIconSchema]>; @@ -59,17 +59,20 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( } render() { - const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc); - const refLayout = Doc.Layout(referenceDoc); - const button = <button className="fontIconBox-outerDiv" ref={this._ref} onContextMenu={this.specificContextMenu} - style={{ - padding: Cast(this.layoutDoc._xPadding, "number", null), - background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)), - boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined - }}> - <FontAwesomeIcon className="fontIconBox-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" /> - {!this.rootDoc.title ? (null) : <div className="fontIconBox-label" style={{ width: this.rootDoc.label ? "max-content" : undefined }}> {StrCast(this.rootDoc.label, StrCast(this.rootDoc.title).substring(0, 6))} </div>} - </button>; + const label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); + const color = StrCast(this.layoutDoc.color, this._foregroundColor); + const backgroundColor = StrCast(this.layoutDoc._backgroundColor, StrCast(this.rootDoc.backgroundColor, this.props.backgroundColor?.(this.rootDoc))); + const shape = StrCast(this.layoutDoc.iconShape, "round"); + const button = <> + <button className={`menuButton-${shape}`} ref={this._ref} onContextMenu={this.specificContextMenu} + style={{ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined, backgroundColor }}> + <div className="menuButton-wrap"> + {<FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={StrCast(this.dataDoc.icon, "user") as any} color={color} + size={this.layoutDoc.iconShape === "square" ? "sm" : "lg"} />} + {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} + </div> + </button> + </>; return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> {button} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 209a3cc6b..983470f9d 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -43,7 +43,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument }, icon: "trash" }); - ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" }); } @undoBatch diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index d8fe47f4e..720af6c9d 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -19,12 +19,15 @@ export class LinkDescriptionPopup extends React.Component<{}> { @action descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - LinkManager.currentLink && (LinkManager.currentLink.description = e.currentTarget.value); + this.description = e.currentTarget.value; } @action - onDismiss = () => { + onDismiss = (add: boolean) => { LinkDescriptionPopup.descriptionPopup = false; + if (add) { + LinkManager.currentLink && (LinkManager.currentLink.description = this.description); + } } @action @@ -50,15 +53,16 @@ export class LinkDescriptionPopup extends React.Component<{}> { left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700, top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, }}> - <input className="linkDescriptionPopup-input" onKeyPress={e => e.key === "Enter" && this.onDismiss()} + <input className="linkDescriptionPopup-input" + onKeyPress={e => e.key === "Enter" && this.onDismiss(true)} placeholder={"(optional) enter link label..."} onChange={(e) => this.descriptionChanged(e)}> </input> <div className="linkDescriptionPopup-btn"> <div className="linkDescriptionPopup-btn-dismiss" - onPointerDown={this.onDismiss}> Dismiss </div> + onPointerDown={e => this.onDismiss(false)}> Dismiss </div> <div className="linkDescriptionPopup-btn-add" - onPointerDown={this.onDismiss}> Add </div> + onPointerDown={e => this.onDismiss(true)}> Add </div> </div> </div>; } diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx index 89602f219..2a3dd8d2d 100644 --- a/src/client/views/nodes/TaskCompletedBox.tsx +++ b/src/client/views/nodes/TaskCompletedBox.tsx @@ -1,7 +1,5 @@ import React = require("react"); import { observer } from "mobx-react"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface } from "../../../fields/Schema"; import "./TaskCompletedBox.scss"; import { observable, action } from "mobx"; import { Fade } from "@material-ui/core"; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index a1e641d4a..1313bded7 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -13,7 +13,7 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; @@ -535,9 +535,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @action highlight = (color: string) => { // creates annotation documents for current highlights - const annotationDoc = this.makeAnnotationDocument(color); - annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc); - return annotationDoc; + const effectiveAcl = GetEffectiveAcl(this.props.Document); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + annotationDoc && this.addDocument?.(annotationDoc); + return annotationDoc ?? undefined; } /** * This is temporary for creating annotations from highlights. It will diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 958a37568..8ae71c035 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -190,7 +190,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb")); list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb")); - alias._pivotField = this._fieldKey; + alias._pivotField = this._fieldKey.startsWith("#") ? "#" : this._fieldKey; this.props.tbox.props.addDocTab(alias, "onRight"); } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 4dbe59e60..e95a4ccd2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1311,7 +1311,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.doLinkOnDeselect(); // move the richtextmenu offscreen - if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); + //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } _lastTimedMark: Mark | undefined = undefined; @@ -1390,7 +1390,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { TraceMobx(); - const scale = this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); + const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), this.props.isSelected() ? 10 : 0); // need to make sure that we update a text box that is selected after updating the one that was deselected diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 7ccbfa051..6e268be48 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -77,7 +77,8 @@ export default class RichTextMenu extends AntimodeMenu { super(props); RichTextMenu.Instance = this; this._canFade = false; - this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); + //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); + this.Pinned = true; this.fontSizeOptions = [ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, @@ -184,11 +185,15 @@ export default class RichTextMenu extends AntimodeMenu { const active = this.getActiveFontStylesOnSelection(); const activeFamilies = active.activeFamilies; const activeSizes = active.activeSizes; + const activeColors = active.activeColors; + const activeHighlights = active.activeHighlights; this.activeListType = this.getActiveListStyle(); this.activeAlignment = this.getActiveAlignment(); this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; this.activeFontSize = !activeSizes.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; + this.activeFontColor = !activeColors.length ? "black" : activeColors.length === 1 ? String(activeColors[0]) : "..."; + this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "..."; // update link in current selection const targetTitle = await this.getTextLinkTargetTitle(); @@ -249,10 +254,12 @@ export default class RichTextMenu extends AntimodeMenu { // finds font sizes and families in selection getActiveFontStylesOnSelection() { - if (!this.view) return { activeFamilies: [], activeSizes: [] }; + if (!this.view) return { activeFamilies: [], activeSizes: [], activeColors: [], activeHighlights: [] }; const activeFamilies: string[] = []; const activeSizes: string[] = []; + const activeColors: string[] = []; + const activeHighlights: string[] = []; if (this.TextView.props.isSelected(true)) { const state = this.view.state; const pos = this.view.state.selection.$from; @@ -260,15 +267,20 @@ export default class RichTextMenu extends AntimodeMenu { if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { ref_node.marks.forEach(m => { m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); + m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); + m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); }); } !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily)))); !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize)))); + !activeColors.length && (activeSizes.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor)))); } !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily))); !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize))); - return { activeFamilies, activeSizes }; + !activeColors.length && (activeColors.push(StrCast(Doc.UserDoc().fontColor, "black"))); + !activeHighlights.length && (activeHighlights.push(StrCast(Doc.UserDoc().fontHighlight, ""))); + return { activeFamilies, activeSizes, activeColors, activeHighlights }; } getMarksInSelection(state: EditorState<any>) { @@ -425,10 +437,16 @@ export default class RichTextMenu extends AntimodeMenu { } changeFontSize = (mark: Mark, view: EditorView) => { + if ((this.view?.state.selection.$from.pos || 0) < 2) { + this.TextView.layoutDoc._fontSize = mark.attrs.fontSize; + } this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch, true); } changeFontFamily = (mark: Mark, view: EditorView) => { + if ((this.view?.state.selection.$from.pos || 0) < 2) { + this.TextView.layoutDoc._fontFamily = mark.attrs.family; + } this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch, true); } @@ -529,7 +547,7 @@ export default class RichTextMenu extends AntimodeMenu { indentParagraph(state: EditorState<any>, dispatch: any) { var tr = state.tr; - let headin = false; + const heading = false; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; @@ -539,7 +557,7 @@ export default class RichTextMenu extends AntimodeMenu { } return true; }); - !headin && dispatch?.(tr); + !heading && dispatch?.(tr); return true; } @@ -605,8 +623,11 @@ export default class RichTextMenu extends AntimodeMenu { label = "No marks are currently stored"; } + //onPointerDown={onBrushClick} + const button = <Tooltip title={<div className="dash-tooltip">style brush</div>} placement="bottom"> - <button className="antimodeMenu-button" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> + + <button className="antimodeMenu-button" style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} /> </button> </Tooltip>; @@ -615,11 +636,11 @@ export default class RichTextMenu extends AntimodeMenu { <div className="dropdown"> <p>{label}</p> <button onPointerDown={this.clearBrush}>Clear brush</button> - <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress}></input> + <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress} /> </div>; return ( - <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} /> + <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> ); } @@ -678,8 +699,9 @@ export default class RichTextMenu extends AntimodeMenu { self.TextView.EditorView!.focus(); } + // onPointerDown={onColorClick} const button = <Tooltip title={<div className="dash-tooltip">set font color</div>} placement="bottom"> - <button className="antimodeMenu-button color-preview-button" onPointerDown={onColorClick}> + <button className="antimodeMenu-button color-preview-button"> <FontAwesomeIcon icon="palette" size="lg" /> <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div> </button> @@ -700,7 +722,7 @@ export default class RichTextMenu extends AntimodeMenu { </div>; return ( - <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} /> + <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> ); } @@ -732,8 +754,9 @@ export default class RichTextMenu extends AntimodeMenu { UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter"); } + //onPointerDown={onHighlightClick} const button = <Tooltip title={<div className="dash-tooltip">set highlight color</div>} placement="bottom"> - <button className="antimodeMenu-button color-preview-button" key="highilghter-button" onPointerDown={onHighlightClick}> + <button className="antimodeMenu-button color-preview-button" key="highilghter-button" > <FontAwesomeIcon icon="highlighter" size="lg" /> <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div> </button> @@ -754,7 +777,7 @@ export default class RichTextMenu extends AntimodeMenu { </div>; return ( - <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} /> + <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> ); } @@ -777,7 +800,9 @@ export default class RichTextMenu extends AntimodeMenu { const link = this.currentLink ? this.currentLink : ""; const button = <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom"> - <div><FontAwesomeIcon icon="link" size="lg" /> </div> + <button className="antimodeMenu-button color-preview-button"> + <FontAwesomeIcon icon="link" size="lg" /> + </button> </Tooltip>; const dropdownContent = @@ -789,7 +814,8 @@ export default class RichTextMenu extends AntimodeMenu { <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> </div>; - return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />; + return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} + openDropdownOnButton={true} link={true} />; } async getTextLinkTargetTitle() { @@ -876,10 +902,11 @@ export default class RichTextMenu extends AntimodeMenu { if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { ref_node = pos.nodeBefore; } - else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - ref_node = pos.nodeAfter; + if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { + if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) + ref_node = pos.nodeAfter; } - else if (pos.pos > 0) { + if (!ref_node && pos.pos > 0) { let skip = false; for (let i: number = pos.pos - 1; i > 0; i--) { this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { @@ -920,16 +947,22 @@ export default class RichTextMenu extends AntimodeMenu { render() { TraceMobx(); const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ - !this.collapsed ? this.getDragger() : (null), - !this.Pinned ? (null) : <div key="frag1"> {[ - this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - <div className="richTextMenu-divider" key="divider" /> - ]}</div>, + //!this.collapsed ? this.getDragger() : (null), + // !this.Pinned ? (null) : <div key="frag1"> {[ + // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + // <div className="richTextMenu-divider" key="divider" /> + // ]}</div>, + this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), this.createColorButton(), this.createHighlighterButton(), this.createLinkButton(), @@ -957,16 +990,16 @@ export default class RichTextMenu extends AntimodeMenu { this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), <div className="richTextMenu-divider" key="divider 5" />,]} </div> - <div key="collapser"> - {/* <div key="collapser"> + {/* <div key="collapser"> + {<div key="collapser"> <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> </button> - </div> */} + </div> } <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> </button> - </div> + </div> */} </div>; return ( @@ -982,6 +1015,7 @@ interface ButtonDropdownProps { button: JSX.Element; dropdownContent: JSX.Element; openDropdownOnButton?: boolean; + link?: boolean; } @observer @@ -1024,18 +1058,10 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { render() { return ( <div className="button-dropdown-wrapper" ref={node => this.ref = node}> - {this.props.openDropdownOnButton ? - <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> - {this.props.button} - <FontAwesomeIcon icon="caret-down" size="sm" /> - </button> : - <> - {this.props.button} - <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> - <FontAwesomeIcon icon="caret-down" size="sm" /> - </button> - </>} - + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> + {this.props.button} + <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> + </div> {this.showDropdown ? this.props.dropdownContent : (null)} </div> ); diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 0eca6d753..1616500f6 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -79,14 +79,14 @@ export const nodes: { [index: string]: NodeSpec } = { { tag: "h5", attrs: { level: 5 } }, { tag: "h6", attrs: { level: 6 } }], toDOM(node) { - var dom = toParagraphDOM(node) as any; - var level = node.attrs.level || 1; + const dom = toParagraphDOM(node) as any; + const level = node.attrs.level || 1; dom[0] = 'h' + level; return dom; }, getAttrs(dom: any) { - var attrs = getParagraphNodeAttrs(dom) as any; - var level = Number(dom.nodeName.substring(1)) || 1; + const attrs = getParagraphNodeAttrs(dom) as any; + const level = Number(dom.nodeName.substring(1)) || 1; attrs.level = level; return attrs; } diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index c3e1ae22f..7bea8d01b 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -93,7 +93,7 @@ export default class PDFMenu extends AntimodeMenu { @computed get highlighter() { const button = - <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={this.highlightClicked}> + <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onPointerDown={this.highlightClicked}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> </button>; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 94a052824..7ab2876da 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -4,7 +4,7 @@ const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; @@ -13,7 +13,7 @@ import { createSchema, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -577,9 +577,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action highlight = (color: string) => { // creates annotation documents for current highlights - const annotationDoc = this.makeAnnotationDocument(color); - annotationDoc && this.props.addDocument?.(annotationDoc); - return annotationDoc; + const effectiveAcl = GetEffectiveAcl(this.props.Document); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + annotationDoc && this.addDocument?.(annotationDoc); + return annotationDoc as Doc ?? undefined; } /** diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 60e935e98..09fbe383c 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1048,12 +1048,12 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc <FontAwesomeIcon icon="object-group" size="lg" /> </span> */} <div style={{ position: "absolute", left: 15 }}>{Doc.CurrentUserEmail}</div> - <FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" style={{ position: "relative", left: 24, padding: 1 }} /> + <FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" style={{ color: "black", position: "relative", left: 24, padding: 1 }} /> <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} - style={{ paddingLeft: 23, width: this._searchbarOpen ? "200px" : "200px" }} /> + style={{ paddingLeft: 23, color: "black", width: this._searchbarOpen ? "200px" : "200px" }} /> </div> - <div style={{ zIndex: 2000 }}> + <div style={{ zIndex: 20000, color: "black" }}> {this._searchbarOpen === true ? <div style={{ display: "flex", justifyContent: "center", }}> <div style={{ diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index b78d5611c..ad6a9ac1d 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -409,7 +409,7 @@ export namespace Doc { // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). export function GetProto(doc: Doc): Doc { if (doc instanceof Promise) { - console.log("GetProto: error: got Promise insead of Doc"); + console.log("GetProto: warning: got Promise insead of Doc"); } const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); return proto === doc ? proto : Doc.GetProto(proto); @@ -508,6 +508,10 @@ export namespace Doc { alias.aliasOf = doc; alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); alias.author = Doc.CurrentUserEmail; + + if (!doc.aliases) doc.aliases = new List<Doc>([alias]); + else Doc.AddDocToList(doc, "aliases", alias); + return alias; } @@ -524,10 +528,10 @@ export namespace Doc { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); const copyObjectField = async (field: ObjectField) => { - const list = await Cast(doc[key], listSpec(Doc)); + const list = Cast(doc[key], listSpec(Doc)); const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); if (docs !== undefined && docs.length) { - const clones = await Promise.all(docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions, dontCreate))); + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, rtfs, exclusions, dontCreate))); !dontCreate && assignKey(new List<Doc>(clones)); } else if (doc[key] instanceof Doc) { assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate)); // reference documents except copy documents that are expanded teplate fields @@ -621,7 +625,7 @@ export namespace Doc { Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]); const docString = JSON.stringify({ id: doc[Id], docs }, replacer); - var zip = new JSZip(); + const zip = new JSZip(); zip.file("doc.json", docString); diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 5cf0e0cc3..2ca5ac082 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -20,7 +20,7 @@ export class RichTextField extends ObjectField { } Empty() { - return !(this.Text || this.Data.toString().includes("dashField")); + return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align")); } [Copy]() { diff --git a/src/fields/util.ts b/src/fields/util.ts index a62795e64..957b2c8cd 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -10,6 +10,7 @@ import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; +import { addSyntheticLeadingComment } from "typescript"; function _readOnlySetter(): never { @@ -74,7 +75,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !playgroundMode; + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode; if (writeToDoc) { if (value === undefined) { @@ -115,22 +116,34 @@ export function OVERRIDE_ACL(val: boolean) { _overrideAcl = val; } -let playgroundMode = false; +// playground mode allows the user to add/delete documents or make layout changes without them saving to the server +// let playgroundMode = false; -export function togglePlaygroundMode() { - playgroundMode = !playgroundMode; -} - -export function getPlaygroundMode() { - return playgroundMode; -} +// export function togglePlaygroundMode() { +// playgroundMode = !playgroundMode; +// } +// the list of groups that the current user is a member of let currentUserGroups: string[] = []; +// called from GroupManager once the groups have been fetched from the server export function setGroups(groups: string[]) { currentUserGroups = groups; } +/** + * These are the various levels of access a user can have to a document. + * + * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. + * + * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. + * + * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything. + * + * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. + * + * None: the document is not shared with that user. + */ export enum SharingPermissions { Admin = "Admin", Edit = "Can Edit", @@ -139,18 +152,23 @@ export enum SharingPermissions { None = "Not Shared" } +/** + * Calculates the effective access right to a document for the current user. + */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { + if (!target) return AclPrivate; if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { - if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin; + // if the current user is the author of the document / the current user is a member of the admin group + // but not if the doc in question is an alias - the current user will be the author of their alias rather than the original author + if ((Doc.CurrentUserEmail === (target.__fields?.author || target.author) && !(target.aliasOf || target.__fields?.aliasOf)) || currentUserGroups.includes("admin")) return AclAdmin; + // if the ACL is being overriden or the property being modified is one of the playground fields (which can be freely modified) if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; let effectiveAcl = AclPrivate; - let aclPresent = false; - const HierarchyMapping = new Map<symbol, number>([ [AclPrivate, 0], [AclReadonly, 1], @@ -160,19 +178,28 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number) ]); for (const [key, value] of Object.entries(target[AclSym])) { + // there are issues with storing fields with . in the name, so they are replaced with _ during creation + // as a result we need to restore them again during this comparison. if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) { - if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) { - aclPresent = true; + if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; - if (effectiveAcl === AclEdit) break; + if (effectiveAcl === AclAdmin) break; } } } - return aclPresent ? effectiveAcl : AclEdit; + // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) + return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl; } return AclAdmin; } - +/** + * Recursively distributes the access right for a user across the children of a document and its annotations. + * @param key the key storing the access right (e.g. ACL-groupname) + * @param acl the access right being stored (e.g. "Can Edit") + * @param target the document on which this access right is being set + * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the ACLs from the collection) + * inheritingFromCollection is not currently being used but could be used if ACL assignment defaults change + */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { const HierarchyMapping = new Map<string, number>([ @@ -183,37 +210,51 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc ["Admin", 4] ]); + let changed = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) const dataDoc = target[DataSym]; - if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl; + // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive + if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) { + target[key] = acl; + changed = true; + + // maps over the aliases of the document + if (target.aliases) { + DocListCast(target.aliases).map(alias => { + distributeAcls(key, acl, alias); + }); + } + + } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { dataDoc[key] = acl; + changed = true; + // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d); - d[key] = acl; + distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data); - data[key] = acl; + distributeAcls(key, acl, data, inheritingFromCollection); } }); + // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d); - d[key] = acl; + distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data); - data[key] = acl; + distributeAcls(key, acl, data, inheritingFromCollection); } }); } + + changed && fetchProto(target); // updates target[AclSym] when changes to acls have been made } const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", @@ -223,6 +264,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an const effectiveAcl = GetEffectiveAcl(target, in_prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; + // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === "string" && prop.startsWith("ACL") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; // if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index 60f66c878..fd9bc0c83 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -1,11 +1,11 @@ -import { readFile, writeFile, exists, mkdir, unlink, createWriteStream } from 'fs'; -import { ExecOptions } from 'shelljs'; import { exec } from 'child_process'; -import * as path from 'path'; -import * as rimraf from "rimraf"; -import { yellow, Color } from 'colors'; +import { Color, yellow } from 'colors'; +import { createWriteStream, exists, mkdir, readFile, unlink, writeFile } from 'fs'; import * as nodemailer from "nodemailer"; import { MailOptions } from "nodemailer/lib/json-transport"; +import * as path from 'path'; +import * as rimraf from "rimraf"; +import { ExecOptions } from 'shelljs'; import Mail = require('nodemailer/lib/mailer'); const projectRoot = path.resolve(__dirname, "../../"); diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts index e2cd88726..e657866ce 100644 --- a/src/server/ApiManagers/UtilManager.ts +++ b/src/server/ApiManagers/UtilManager.ts @@ -6,7 +6,6 @@ import { exec } from 'child_process'; // const recommender = new Recommender(); // recommender.testModel(); -import executeImport from "../../scraping/buxton/final/BuxtonImporter"; export default class UtilManager extends ApiManager { diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 2bf4c1956..890fb6f6d 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -1,23 +1,22 @@ -import { unlinkSync, createWriteStream, readFileSync, rename, writeFile, existsSync } from 'fs'; -import { Utils } from '../Utils'; -import * as path from 'path'; -import * as sharp from 'sharp'; -import request = require('request-promise'); +import { red } from 'colors'; import { ExifImage } from 'exif'; -import { Opt } from '../fields/Doc'; -import { AcceptibleMedia, Upload } from './SharedMediaTypes'; -import { filesDirectory, publicDirectory } from '.'; import { File } from 'formidable'; +import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs'; +import * as path from 'path'; import { basename } from "path"; -import { createIfNotExists } from './ActionUtilities'; +import * as sharp from 'sharp'; +import { Stream } from 'stream'; +import { filesDirectory, publicDirectory } from '.'; +import { Opt } from '../fields/Doc'; import { ParsedPDF } from "../server/PdfTypes"; +import { Utils } from '../Utils'; +import { createIfNotExists } from './ActionUtilities'; +import { clientPathToFile, Directory, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager'; +import { resolvedServerUrl } from "./server_Initialization"; +import { AcceptibleMedia, Upload } from './SharedMediaTypes'; +import request = require('request-promise'); const parse = require('pdf-parse'); -import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager'; -import { red } from 'colors'; -import { Stream } from 'stream'; -import { resolvedPorts } from './server_Initialization'; const requestImageSize = require("../client/util/request-image-size"); -import { resolvedServerUrl } from "./server_Initialization"; export enum SizeSuffix { Small = "_s", diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 24745cbb4..a9a3b0481 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -1,9 +1,9 @@ -import { Database } from './database'; - -import * as path from 'path'; import * as fs from 'fs'; +import * as path from 'path'; +import { Database } from './database'; import { Search } from './Search'; + function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { for (const key in doc) { if (!doc.hasOwnProperty(key)) { diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts index 1f1d702d9..d2d8bb3b3 100644 --- a/src/server/MemoryDatabase.ts +++ b/src/server/MemoryDatabase.ts @@ -1,6 +1,6 @@ -import { IDatabase, DocumentsCollection } from './IDatabase'; -import { Transferable } from './Message'; import * as mongodb from 'mongodb'; +import { DocumentsCollection, IDatabase } from './IDatabase'; +import { Transferable } from './Message'; export class MemoryDatabase implements IDatabase { diff --git a/src/server/Message.ts b/src/server/Message.ts index ff0381fd3..59b24cd82 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,6 +1,6 @@ -import { Utils } from "../Utils"; import { Point } from "../pen-gestures/ndollar"; import { AnalysisResult, ImportResults } from "../scraping/buxton/final/BuxtonImporter"; +import { Utils } from "../Utils"; export class Message<T> { private _name: string; diff --git a/src/server/ProcessFactory.ts b/src/server/ProcessFactory.ts index acb8b3a99..63682368f 100644 --- a/src/server/ProcessFactory.ts +++ b/src/server/ProcessFactory.ts @@ -1,8 +1,8 @@ -import { existsSync, mkdirSync } from "fs"; -import { pathFromRoot, fileDescriptorFromStream } from './ActionUtilities'; -import rimraf = require("rimraf"); import { ChildProcess, spawn, StdioOptions } from "child_process"; +import { existsSync, mkdirSync } from "fs"; import { Stream } from "stream"; +import { fileDescriptorFromStream, pathFromRoot } from './ActionUtilities'; +import rimraf = require("rimraf"); export namespace ProcessFactory { diff --git a/src/server/Recommender.ts b/src/server/Recommender.ts deleted file mode 100644 index 935ec3871..000000000 --- a/src/server/Recommender.ts +++ /dev/null @@ -1,133 +0,0 @@ -// //import { Doc } from "../fields/Doc"; -// //import { StrCast } from "../fields/Types"; -// //import { List } from "../fields/List"; -// //import { CognitiveServices } from "../client/cognitive_services/CognitiveServices"; - -// // var w2v = require('word2vec'); -// var assert = require('assert'); -// var arxivapi = require('arxiv-api-node'); -// import requestPromise = require("request-promise"); -// import * as use from '@tensorflow-models/universal-sentence-encoder'; -// import { Tensor } from "@tensorflow/tfjs-core/dist/tensor"; -// require('@tensorflow/tfjs-node'); - -// //http://gnuwin32.sourceforge.net/packages/make.htm - -// export class Recommender { - -// private _model: any; -// static Instance: Recommender; -// private dimension: number = 0; -// private choice: string = ""; // Tensorflow or Word2Vec - -// constructor() { -// Recommender.Instance = this; -// } - -// /*** -// * Loads pre-trained model from TF -// */ - -// public async loadTFModel() { -// let self = this; -// return new Promise(res => { -// use.load().then(model => { -// self.choice = "TF"; -// self._model = model; -// self.dimension = 512; -// res(model); -// }); -// } - -// ); -// } - -// /*** -// * Loads pre-trained model from word2vec -// */ - -// // private loadModel(): Promise<any> { -// // let self = this; -// // return new Promise(res => { -// // w2v.loadModel("./node_modules/word2vec/examples/fixtures/vectors.txt", function (err: any, model: any) { -// // self.choice = "WV"; -// // self._model = model; -// // self.dimension = model.size; -// // res(model); -// // }); -// // }); -// // } - -// /*** -// * Testing -// */ - -// public async testModel() { -// if (!this._model) { -// await this.loadTFModel(); -// } -// if (this._model) { -// if (this.choice === "WV") { -// let similarity = this._model.similarity('father', 'mother'); -// } -// else if (this.choice === "TF") { -// const model = this._model as use.UniversalSentenceEncoder; -// // Embed an array of sentences. -// const sentences = [ -// 'Hello.', -// 'How are you?' -// ]; -// const embeddings = await this.vectorize(sentences); -// if (embeddings) embeddings.print(true /*verbose*/); -// // model.embed(sentences).then(embeddings => { -// // // `embeddings` is a 2D tensor consisting of the 512-dimensional embeddings for each sentence. -// // // So in this example `embeddings` has the shape [2, 512]. -// // embeddings.print(true /* verbose */); -// // }); -// } -// } -// else { -// console.log("model not found :("); -// } -// } - -// /*** -// * Uses model to convert words to vectors -// */ - -// public async vectorize(text: string[]): Promise<Tensor | undefined> { -// if (!this._model) { -// await this.loadTFModel(); -// } -// if (this._model) { -// if (this.choice === "WV") { -// let word_vecs = this._model.getVectors(text); -// return word_vecs; -// } -// else if (this.choice === "TF") { -// const model = this._model as use.UniversalSentenceEncoder; -// return new Promise<Tensor>(res => { -// model.embed(text).then(embeddings => { -// res(embeddings); -// }); -// }); - -// } -// } -// } - -// // public async trainModel() { -// // w2v.word2vec("./node_modules/word2vec/examples/eng_news-typical_2016_1M-sentences.txt", './node_modules/word2vec/examples/my_phrases.txt', { -// // cbow: 1, -// // size: 200, -// // window: 8, -// // negative: 25, -// // hs: 0, -// // sample: 1e-4, -// // threads: 20, -// // iter: 200, -// // minCount: 2 -// // }); -// // } - -// } diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index 1a2340afc..78b75d6be 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -1,8 +1,8 @@ -import RouteSubscriber from "./RouteSubscriber"; -import { DashUserModel } from "./authentication/DashUserModel"; -import { Request, Response, Express } from 'express'; -import { cyan, red, green } from 'colors'; +import { cyan, green, red } from 'colors'; +import { Express, Request, Response } from 'express'; import { AdminPriviliges } from "."; +import { DashUserModel } from "./authentication/DashUserModel"; +import RouteSubscriber from "./RouteSubscriber"; export enum Method { GET, diff --git a/src/server/Search.ts b/src/server/Search.ts index 21064e520..decd1f5b1 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -1,5 +1,5 @@ -import * as rp from 'request-promise'; import { red } from 'colors'; +import * as rp from 'request-promise'; const pathTo = (relative: string) => `http://localhost:8983/solr/dash/${relative}`; diff --git a/src/server/database.ts b/src/server/database.ts index 2372cbcf2..b7aa77f5d 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -1,11 +1,11 @@ import * as mongodb from 'mongodb'; -import { Transferable } from './Message'; +import * as mongoose from 'mongoose'; import { Opt } from '../fields/Doc'; -import { Utils, emptyFunction } from '../Utils'; +import { emptyFunction, Utils } from '../Utils'; import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; -import { IDatabase, DocumentsCollection } from './IDatabase'; +import { DocumentsCollection, IDatabase } from './IDatabase'; import { MemoryDatabase } from './MemoryDatabase'; -import * as mongoose from 'mongoose'; +import { Transferable } from './Message'; import { Upload } from './SharedMediaTypes'; export namespace Database { diff --git a/src/server/downsize.ts b/src/server/downsize.ts index 5cd709fa3..382994e2d 100644 --- a/src/server/downsize.ts +++ b/src/server/downsize.ts @@ -1,5 +1,5 @@ -import * as sharp from 'sharp'; import * as fs from 'fs'; +import * as sharp from 'sharp'; const folder = "./src/server/public/files/"; const pngTypes = ["png", "PNG"]; diff --git a/src/server/index.ts b/src/server/index.ts index 9af4b00bc..9185e3c5e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,29 +1,29 @@ require('dotenv').config(); -import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; +import { yellow } from "colors"; import * as mobileDetect from 'mobile-detect'; import * as path from 'path'; -import { Database } from './database'; -import { DashUploadUtils } from './DashUploadUtils'; -import RouteSubscriber from './RouteSubscriber'; -import initializeServer, { resolvedPorts } from './server_Initialization'; -import RouteManager, { Method, _success, _permission_denied, _error, _invalid, PublicHandler } from './RouteManager'; import * as qs from 'query-string'; -import UtilManager from './ApiManagers/UtilManager'; -import { SearchManager } from './ApiManagers/SearchManager'; -import UserManager from './ApiManagers/UserManager'; -import DownloadManager from './ApiManagers/DownloadManager'; -import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; -import DeleteManager from "./ApiManagers/DeleteManager"; -import PDFManager from "./ApiManagers/PDFManager"; -import UploadManager from "./ApiManagers/UploadManager"; import { log_execution } from "./ActionUtilities"; +import DeleteManager from "./ApiManagers/DeleteManager"; +import DownloadManager from './ApiManagers/DownloadManager'; import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; -import HypothesisManager from "./ApiManagers/HypothesisManager"; import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; -import { Logger } from "./ProcessFactory"; -import { yellow } from "colors"; +import HypothesisManager from "./ApiManagers/HypothesisManager"; +import PDFManager from "./ApiManagers/PDFManager"; +import { SearchManager } from './ApiManagers/SearchManager'; import SessionManager from "./ApiManagers/SessionManager"; +import UploadManager from "./ApiManagers/UploadManager"; +import UserManager from './ApiManagers/UserManager'; +import UtilManager from './ApiManagers/UtilManager'; +import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; +import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; +import { DashUploadUtils } from './DashUploadUtils'; +import { Database } from './database'; +import { Logger } from "./ProcessFactory"; +import RouteManager, { Method, PublicHandler } from './RouteManager'; +import RouteSubscriber from './RouteSubscriber'; +import initializeServer, { resolvedPorts } from './server_Initialization'; export const AdminPriviliges: Map<string, boolean> = new Map(); export const onWindows = process.platform === "win32"; diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 744d4547b..e40f2b8e5 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -1,31 +1,31 @@ +import * as bodyParser from 'body-parser'; +import { blue, yellow } from 'colors'; +import * as cookieParser from 'cookie-parser'; +import * as cors from "cors"; import * as express from 'express'; -import * as expressValidator from 'express-validator'; import * as session from 'express-session'; +import * as expressValidator from 'express-validator'; +import * as fs from 'fs'; +import { Server as HttpServer } from "http"; +import { createServer, Server as HttpsServer } from "https"; import * as passport from 'passport'; -import * as bodyParser from 'body-parser'; -import * as cookieParser from 'cookie-parser'; -import expressFlash = require('express-flash'); -import flash = require('connect-flash'); -import { Database } from './database'; -import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; -const MongoStore = require('connect-mongo')(session); -import RouteManager from './RouteManager'; -import { WebSocket } from './websocket'; +import * as request from 'request'; import * as webpack from 'webpack'; -const config = require('../../webpack.config'); -const compiler = webpack(config); import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; -import * as fs from 'fs'; -import * as request from 'request'; -import RouteSubscriber from './RouteSubscriber'; import { publicDirectory } from '.'; import { logPort } from './ActionUtilities'; -import { blue, yellow } from 'colors'; -import * as cors from "cors"; -import { createServer, Server as HttpsServer } from "https"; -import { Server as HttpServer } from "http"; import { SSL } from './apis/google/CredentialsLoader'; +import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; +import { Database } from './database'; +import RouteManager from './RouteManager'; +import RouteSubscriber from './RouteSubscriber'; +import { WebSocket } from './websocket'; +import expressFlash = require('express-flash'); +import flash = require('connect-flash'); +const MongoStore = require('connect-mongo')(session); +const config = require('../../webpack.config'); +const compiler = webpack(config); /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ diff --git a/src/server/websocket.ts b/src/server/websocket.ts index c80056579..cdce41a3a 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -1,21 +1,20 @@ -import * as fs from 'fs'; -import { logPort } from './ActionUtilities'; +import * as express from "express"; +import { blue, green } from "colors"; +import { createServer, Server } from "https"; +import { networkInterfaces } from "os"; +import * as sio from 'socket.io'; +import { Socket } from "socket.io"; +import executeImport from "../scraping/buxton/final/BuxtonImporter"; import { Utils } from "../Utils"; -import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } from "./Message"; +import { logPort } from './ActionUtilities'; +import { timeMap } from "./ApiManagers/UserManager"; +import { GoogleCredentialsLoader, SSL } from "./apis/google/CredentialsLoader"; +import YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Client } from "./Client"; -import { Socket } from "socket.io"; import { Database } from "./database"; -import { Search } from "./Search"; -import * as sio from 'socket.io'; -import YoutubeApi from "./apis/youtube/youtubeApiSample"; -import { GoogleCredentialsLoader, SSL } from "./apis/google/CredentialsLoader"; -import { timeMap } from "./ApiManagers/UserManager"; -import { green } from "colors"; -import { networkInterfaces } from "os"; -import executeImport from "../scraping/buxton/final/BuxtonImporter"; import { DocumentsCollection } from "./IDatabase"; -import { createServer, Server } from "https"; -import * as express from "express"; +import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from "./Message"; +import { Search } from "./Search"; import { resolvedPorts } from './server_Initialization'; export namespace WebSocket { @@ -187,7 +186,7 @@ export namespace WebSocket { + currentdate.getHours() + ":" + currentdate.getMinutes() + ":" + currentdate.getSeconds(); - console.log(green(`user ${userEmail} has connected to the web socket at: ${datetime}`)); + console.log(blue(`user ${userEmail} has connected to the web socket at: ${datetime}`)); socketMap.set(socket, userEmail); } @@ -209,10 +208,12 @@ export namespace WebSocket { } function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { + process.stdout.write(`.`); Database.Instance.getDocument(id, callback); } function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { + process.stdout.write(`${ids.length}…`); Database.Instance.getDocuments(ids, callback); } diff --git a/webpack.config.js b/webpack.config.js index a5fe6ad80..c973be1ed 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -66,42 +66,42 @@ module.exports = { }, module: { rules: [{ - test: [/\.tsx?$/], - use: [{ - loader: 'ts-loader', - options: { - transpileOnly: true - } - }] + test: [/\.tsx?$/], + use: [{ + loader: 'ts-loader', + options: { + transpileOnly: true + } + }] + }, + { + test: /\.scss|css$/, + use: [{ + loader: "style-loader" }, { - test: /\.scss|css$/, - use: [{ - loader: "style-loader" - }, - { - loader: "css-loader" - }, - { - loader: "sass-loader" - } - ] + loader: "css-loader" }, { - test: /\.(jpg|png|pdf)$/, - use: [{ - loader: 'file-loader' - }] - }, - { - test: /\.(png|jpg|gif)$/i, - use: [{ - loader: 'url-loader', - options: { - limit: 8192 - } - }] + loader: "sass-loader" } + ] + }, + { + test: /\.(jpg|png|pdf)$/, + use: [{ + loader: 'file-loader' + }] + }, + { + test: /\.(png|jpg|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + limit: 8192 + } + }] + } ] }, plugins, |