diff options
48 files changed, 831 insertions, 954 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 59278d2af..e498a7cca 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,6 +1,6 @@ import * as io from 'socket.io-client'; import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message"; -import { Opt, Doc, UpdatingFromServer } from '../fields/Doc'; +import { Opt, Doc, UpdatingFromServer, updateCachedAcls } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; @@ -61,6 +61,9 @@ export namespace DocServer { DocServer.PlaygroundFields = livePlaygroundFields; livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground)); } + export function IsPlaygroundField(field: string) { + return DocServer.PlaygroundFields?.includes(field.replace(/^_/, "")); + } export function setFieldWriteMode(field: string, writeMode: WriteMode) { fieldWriteModes[field] = writeMode; @@ -395,7 +398,7 @@ export namespace DocServer { (_cache[field.id] as any).then((f: any) => fieldMap[field.id] = f); } else if (field) { proms.push(_cache[field.id] as any); - fieldMap[field.id] = field; + fieldMap[field.id] = DocServer.GetCachedRefField(field.id) || field; } } }); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a15b017bd..36cbd2059 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,7 +1,7 @@ import { action, runInAction } from "mobx"; import { basename, extname } from "path"; import { DateField } from "../../fields/DateField"; -import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing } from "../../fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing, updateCachedAcls } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { HtmlField } from "../../fields/HtmlField"; import { InkField } from "../../fields/InkField"; @@ -588,7 +588,7 @@ export namespace Docs { dataProps.creationDate = new DateField; dataProps[`${fieldKey}-lastModified`] = new DateField; dataProps["acl-Override"] = "None"; - dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; dataProps[fieldKey] = data; @@ -599,13 +599,15 @@ export namespace Docs { viewProps.author = Doc.CurrentUserEmail; viewProps["acl-Override"] = "None"; - viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + viewProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true); ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); !Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) && !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); + updateCachedAcls(dataDoc); + updateCachedAcls(viewDoc); return viewDoc; } @@ -710,7 +712,7 @@ export namespace Docs { I.author = Doc.CurrentUserEmail; I.rotation = 0; I.data = new InkField(points); - I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; I["acl-Override"] = "None"; I[Initializing] = false; return I; @@ -732,15 +734,20 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options }); } - export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id); - } - export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Freeform }, id); documents.map(d => d.context = inst); return inst; } + + export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id); + } + + export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + } + export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); } @@ -1044,8 +1051,8 @@ export namespace DocUtils { title: ComputedField.MakeFunction("generateLinkTitle(self)") as any, "anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined, "anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined, - "acl-Public": SharingPermissions.Add, - "_acl-Public": SharingPermissions.Add, + "acl-Public": SharingPermissions.Augment, + "_acl-Public": SharingPermissions.Augment, layout_linkView: Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null), linkDisplay: true, hidden: true, linkRelationship, diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts index fddf317bc..575c984f5 100644 --- a/src/client/documents/Gitlike.ts +++ b/src/client/documents/Gitlike.ts @@ -1,7 +1,8 @@ -import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc"; import { List } from "../../fields/List"; -import { ObjectField } from "../../fields/ObjectField"; import { Cast, DateCast } from "../../fields/Types"; +import { DateField } from "../../fields/DateField"; +import { Id } from "../../fields/FieldSymbols"; // synchs matching documents on the two branches that are being merged/pulled // currently this just synchs the main 'fieldKey' component of the data since @@ -10,11 +11,22 @@ function GitlikeSynchDocs(bd: Doc, md: Doc) { const fieldKey = Doc.LayoutFieldKey(md); const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; - if (bdate === mdate || bdate > mdate) return; const bdproto = bd && Doc.GetProto(bd); + if (bdate !== mdate && bdate <= mdate) { + if (bdproto && md) { + bdproto[fieldKey] = Field.Copy(md[fieldKey]); + bdproto[`${fieldKey}-lastModified`] = new DateField(); + } + } + const bldate = DateCast(bd._lastModified)?.date; + const mldate = DateCast(md._lastModified)?.date; + if (bldate === mldate || bldate > mldate) return; if (bdproto && md) { - bdproto[fieldKey] = ObjectField.MakeCopy(md[fieldKey] as ObjectField); - bdproto[`${fieldKey}-lastModified`] = ObjectField.MakeCopy(md[`${fieldKey}-lastModified`] as ObjectField); + bd.x = Field.Copy(md.x); + bd.y = Field.Copy(md.y); + bd.width = Field.Copy(md.width); + bd.height = Field.Copy(md.height); + bdproto._lastModified = new DateField(); } } @@ -36,8 +48,9 @@ async function GitlikePullFromMaster(branch: Doc, suffix = "") { const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); bd && GitlikeSynchDocs(bd, md); }); + const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch); // make branch clones of them, then add them to the branch - const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); + const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []); newlyBranchedDocs.forEach(nd => { Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); nd.context = branch; @@ -56,22 +69,26 @@ async function GitlikeMergeWithMaster(master: Doc, suffix = "") { branches?.map(async branch => { const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); branchChildren && await Promise.all(branchChildren.map(async bd => { + const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch); // see if the branch's child exists on master. - const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone; + const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone; // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields // on the branch child and master clone. if (masterChild.branchOf) { const branchDocProto = Doc.GetProto(bd); const masterChildProto = Doc.GetProto(masterChild); - masterChildProto.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' + const branchTitle = bd.title; + branchDocProto.title = masterChildProto.title; + masterChildProto.title = branchTitle; + masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list. branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child } Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) masterChild.context = master; - GitlikeSynchDocs(Doc.GetProto(masterChild), bd); + GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd); })); const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); masterChildren?.forEach(mc => { // see if any master children @@ -93,6 +110,7 @@ export async function BranchTask(target: Doc, action: "pull" | "merge") { const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster; await func(target, ""); await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations")); + await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar")); } export async function BranchCreate(target: Doc) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ecce573a1..620602070 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -47,6 +47,7 @@ export class CurrentUserUtils { //TODO tfs: these should be temporary... private static mainDocId: string | undefined; + public static searchBtn: Doc; public static get id() { return this.curr_id; } public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } @@ -56,6 +57,7 @@ export class CurrentUserUtils { @observable public static GuestDashboard: Doc | undefined; @observable public static GuestMobile: Doc | undefined; @observable public static propertiesWidth: number = 0; + @observable public static searchPanelWidth: number = 0; // sets up the default User Templates - slideView, headerView static setupUserTemplateButtons(doc: Doc) { @@ -467,7 +469,7 @@ export class CurrentUserUtils { ((doc.emptyButton as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyWebpage === undefined) { - doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, isTemplateDoc: true, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.activeMobileMenu === undefined) { this.setupActiveMobileMenu(doc); @@ -539,6 +541,7 @@ export class CurrentUserUtils { static async menuBtnDescriptions(doc: Doc) { return [ { title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' }, + { title: "Search", target: Cast(doc.mySearchPanel, Doc, null), icon: "search", click: 'selectMainMenu(self)' }, { title: "My Files", target: Cast(doc.myFilesystem, Doc, null), icon: "file", click: 'selectMainMenu(self)' }, { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' }, { title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' }, @@ -551,14 +554,6 @@ export class CurrentUserUtils { ]; } - static setupSearchPanel(doc: Doc) { - if (doc.mySearchPanelDoc === undefined) { - doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({ - _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true, - childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true - })) as any as Doc; - } - } static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { if (doc.menuStack === undefined) { await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing @@ -578,10 +573,23 @@ export class CurrentUserUtils { _height: 60, watchedDocuments, onClick: ScriptField.MakeScript(click, { scriptContext: "any" }) - })); + }) + ); + + menuBtns.forEach(menuBtn => { + if (menuBtn.title == "Search") { + this.searchBtn = menuBtn; + } + }); // hack -- last button is assumed to be the userDoc menuBtns[menuBtns.length - 1].hidden = ComputedField.MakeFunction("IsNoviceMode()"); + menuBtns.forEach(menuBtn => { + if (menuBtn.title == "Search") { + doc.searchBtn = menuBtn + } + }) + doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { title: "menuItemPanel", childDropAction: "alias", @@ -774,8 +782,13 @@ export class CurrentUserUtils { _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true })); const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); - (doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]); - (doc.myDashboards as any as Doc).contextMenuLabels = new List<string>(["Create New Dashboard"]); + const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`); + const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); + const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`); + const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`); + const removeDashboard = ScriptField.MakeScript('removeDashboard(self)'); + (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]); + (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]); } return doc.myDashboards as any as Doc; } @@ -827,6 +840,7 @@ export class CurrentUserUtils { (doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); } } + static setupFilterDocs(doc: Doc) { // setup Filter item if (doc.currentFilter === undefined) { @@ -1038,9 +1052,10 @@ export class CurrentUserUtils { let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId); if (!linkDocs) { linkDocs = new Doc(linkDatabaseId, true); + (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail; (linkDocs as Doc).author = Doc.CurrentUserEmail; (linkDocs as Doc).data = new List<Doc>([]); - (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add; + (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment; } doc.myLinkDatabase = new PrefetchProxy(linkDocs); } @@ -1049,13 +1064,18 @@ export class CurrentUserUtils { if (!sharedDocs) { sharedDocs = Docs.Create.TreeDocument([], { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, - _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add, + _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, _chromeHidden: true, boxShadow: "0 0", }, sharingDocumentId + "outer", sharingDocumentId); - (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Add; + (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment; } if (sharedDocs instanceof Doc) { Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)"; + const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); + const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name }); + sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]); + sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]); + sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]); } doc.mySharedDocs = new PrefetchProxy(sharedDocs); } @@ -1076,6 +1096,16 @@ export class CurrentUserUtils { } } + // Search sidebar is where searches within the document are performed + static setupSearchSidebar(doc: Doc) { + if (doc.mySearchPanel === undefined) { + doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({ + backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true, + childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Sidebar", system: true + })) as any as Doc; + } + } + static setupClickEditorTemplates(doc: Doc) { if (doc["clickFuncs-child"] === undefined) { // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target @@ -1152,7 +1182,7 @@ export class CurrentUserUtils { doc.fontFamily = StrCast(doc.fontFamily, "Arial"); doc.fontColor = StrCast(doc.fontColor, "black"); doc.fontHighlight = StrCast(doc.fontHighlight, ""); - doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, true); + doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, false); doc.activeCollectionBackground = StrCast(doc.activeCollectionBackground, "white"); doc.activeCollectionNestedBackground = Cast(doc.activeCollectionNestedBackground, "string", null); doc.noviceMode = BoolCast(doc.noviceMode, true); @@ -1164,8 +1194,8 @@ export class CurrentUserUtils { this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon this.setupDocTemplates(doc); // sets up the template menu of templates this.setupImportSidebar(doc); + this.setupSearchSidebar(doc); // sets up the search sidebar this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile - this.setupSearchPanel(doc); this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels @@ -1351,15 +1381,6 @@ export class CurrentUserUtils { Doc.AddDocToList(myPresentations, "data", presentation); userDoc.activePresentation = presentation; - const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`); - const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`); - const createDashboard = ScriptField.MakeScript(`createNewDashboard()`); - const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`); - const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); - const removeDashboard = ScriptField.MakeScript('removeDashboard(self)'); - dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!, shareDashboard!, addToDashboards!, removeDashboard!]); - dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard", "Share Dashboard", "Add to Dashboards", "Remove Dashboard"]); Doc.AddDocToList(dashboards, "data", dashboardDoc); CurrentUserUtils.openDashboard(userDoc, dashboardDoc); @@ -1429,17 +1450,15 @@ Scripting.addGlobal(async function addToDashboards(dashboard: Doc) { const allDocs = await DocListCastAsync(dashboard[DataSym]["data-all"]); // moves the data-all field from the datadoc to the layoutdoc, necessary for off screen docs tab to function properly - dashboard["data-all"] = new List<Doc>(allDocs); - dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc))); - - const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig)); - dockingConfig.content = []; - dashboardAlias.dockingConfig = JSON.stringify(dockingConfig); + // dashboard["data-all"] = new List<Doc>(allDocs); + // dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc))); + // const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig)); + // dashboardAlias.dockingConfig = JSON.stringify(dockingConfig); dashboardAlias.data = new List<Doc>(DocListCast(dashboard.data).map(tabFolder => Doc.MakeAlias(tabFolder))); DocListCast(dashboardAlias.data).forEach(doc => doc.dashboard = dashboardAlias); - DocListCast(dashboardAlias.data)[0].data = new List<Doc>(); + //new List<Doc>(); DocListCast(dashboardAlias.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any; Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias); CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 9c6100249..cb0ee411c 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -144,9 +144,11 @@ export class DocumentManager { originalTarget = originalTarget ?? targetDoc; const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; const docView = getFirstDocView(targetDoc, originatingDoc); + const wasHidden = targetDoc.hidden; // + if (wasHidden) runInAction(() => targetDoc.hidden = false); // if the target is hidden, un-hide it here. const focusAndFinish = (didFocus: boolean) => { if (originatingDoc?.isPushpin) { - if (!didFocus || targetDoc.hidden) { + if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal targetDoc.hidden = !targetDoc.hidden; } } else { @@ -161,13 +163,14 @@ export class DocumentManager { const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined; const targetDocContext = contextDoc || annotatedDoc; - const targetDocContextView = targetDocContext && getFirstDocView(targetDocContext); + const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || + (wasHidden && annoContainerView);// if we have an annotation container and the target was hidden, then try again because we just un-hid the document above const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; if (!docView && annoContainerView) { annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below } if (focusView) { - focusView && Doc.linkFollowHighlight(focusView.rootDoc); + Doc.linkFollowHighlight(focusView.rootDoc); focusView.focus(targetDoc, { originalTarget, willZoom, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index ba935e3bf..e009fb3a9 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -210,7 +210,8 @@ export namespace InteractionUtils { style={{ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, fill: fill ? fill : "none", - opacity: strokeWidth !== width ? 0.5 : undefined, + opacity: 1.0, + // opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", strokeWidth: strokeWidth, @@ -299,8 +300,6 @@ export namespace InteractionUtils { return points; case "circle": - - const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); @@ -315,7 +314,6 @@ export namespace InteractionUtils { points.push({ X: newX, Y: y }); } points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) }); - } else { for (var x = Math.min(left, right); x < Math.max(left, right); x++) { const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; @@ -327,7 +325,6 @@ export namespace InteractionUtils { points.push({ X: x, Y: newY }); } points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY }); - } return points; // case "arrow": diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 08f4ac9b7..933b98a8c 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,4 +1,4 @@ -import { observable, observe, action } from "mobx"; +import { observable, observe, action, reaction, computed } from "mobx"; import { computedFn } from "mobx-utils"; import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; @@ -26,36 +26,41 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo export class LinkManager { @observable static _instance: LinkManager; - @observable static userDocs: Doc[] = []; + @observable static userLinkDBs: Doc[] = []; public static currentLink: Opt<Doc>; public static get Instance() { return LinkManager._instance; } + public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb); + static links: Doc[] = []; constructor() { LinkManager._instance = this; setTimeout(() => { - LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)]; - const addLinkToDoc = action((link: Doc): any => { + LinkManager.userLinkDBs = []; + const addLinkToDoc = (link: Doc) => { const a1 = link?.anchor1; const a2 = link?.anchor2; - if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link))); - if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].add(link); - Doc.GetProto(a2)[DirectLinksSym].add(link); - Doc.GetProto(link)[DirectLinksSym].add(link); - } - }); - const remLinkFromDoc = action((link: Doc): any => { + Promise.all([a1, a2]).then(action(() => { + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + Doc.GetProto(link)[DirectLinksSym].add(link); + } + })); + } + const remLinkFromDoc = (link: Doc) => { const a1 = link?.anchor1; const a2 = link?.anchor2; - if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link))); - if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].delete(link); - Doc.GetProto(a2)[DirectLinksSym].delete(link); - Doc.GetProto(link)[DirectLinksSym].delete(link); - } - }); - const watchUserLinks = (userLinks: List<Doc>) => { + Promise.all([a1, a2]).then(action(() => { + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].delete(link); + Doc.GetProto(a2)[DirectLinksSym].delete(link); + Doc.GetProto(link)[DirectLinksSym].delete(link); + } + })); + } + const watchUserLinkDB = (userLinkDBDoc: Doc) => { + LinkManager.links.push(...DocListCast(userLinkDBDoc.data)); const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields - observe(userLinks, change => { + observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) switch (change.type as any) { case "splice": (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); @@ -64,13 +69,29 @@ export class LinkManager { case "update": //let oldValue = change.oldValue; } }, true); + observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) + change => { + switch (change.type as any) { + case "update": + Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => { + const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length); + const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length); + + const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); + const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); + added?.forEach((link: any) => addLinkToDoc(toRealField(link))); + removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); + }); + } + }, true); }; - observe(LinkManager.userDocs, change => { + observe(LinkManager.userLinkDBs, change => { switch (change.type as any) { - case "splice": (change as any).added.forEach(watchUserLinks); break; + case "splice": (change as any).added.forEach(watchUserLinkDB); break; case "update": //let oldValue = change.oldValue; } }, true); + LinkManager.addLinkDB(Doc.LinkDBDoc()); }); } @@ -135,7 +156,8 @@ export class LinkManager { const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc); docViewProps.addDocTab(doc, where); setTimeout(() => { - const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); + const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; + const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. if (targDocView) { targDocView.props.focus(doc, { willZoom: BoolCast(sourceDoc.followLinkZoom, false), diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 3987497b8..bd91db779 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -268,7 +268,8 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button> <div className="default-acl"> - <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} /> + <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} + onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} /> <div className="acl-text">Default access private</div> </div> </div> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index d283510b7..6c4556250 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import Select from "react-select"; import * as RequestPromise from "request-promise"; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; +import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from "../../fields/Doc"; import { List } from "../../fields/List"; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util"; @@ -23,6 +23,7 @@ import { GroupManager, UserOptions } from "./GroupManager"; import { GroupMemberView } from "./GroupMemberView"; import { SelectionManager } from "./SelectionManager"; import "./SharingManager.scss"; +import { LinkManager } from "./LinkManager"; export interface User { email: string; @@ -85,7 +86,8 @@ export class SharingManager extends React.Component<{}> { private AclMap = new Map<symbol, string>([ [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], - [AclAddonly, SharingPermissions.Add], + [AclAugment, SharingPermissions.Augment], + [AclSelfEdit, SharingPermissions.SelfEdit], [AclEdit, SharingPermissions.Edit], [AclAdmin, SharingPermissions.Admin] ]); @@ -101,7 +103,7 @@ export class SharingManager extends React.Component<{}> { this.targetDoc = target_doc || target?.props.Document; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; - this.permissions = SharingPermissions.Add; + this.permissions = SharingPermissions.Augment; }); } @@ -153,10 +155,11 @@ export class SharingManager extends React.Component<{}> { } }); return Promise.all(evaluating).then(() => { - runInAction(() => { + runInAction(async () => { for (const sharer of sharingDocs) { if (!this.users.find(user => user.user.email === sharer.user.email)) { this.users.push(sharer); + LinkManager.addLinkDB(sharer.linkDatabase); } } }); @@ -366,10 +369,10 @@ export class SharingManager extends React.Component<{}> { const dropdownValues: string[] = Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift("-multiple-"); if (override) dropdownValues.unshift("None"); - return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => + return dropdownValues.filter(permission => !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission => ( <option key={permission} value={permission}> - {permission === SharingPermissions.Add ? "Can Augment" : permission} + {permission} </option> ) ); @@ -546,7 +549,7 @@ export class SharingManager extends React.Component<{}> { </select> ) : ( <div className={"permissions-dropdown"}> - {permissions === SharingPermissions.Add ? "Can Augment" : permissions} + {permissions} </div> )} </div> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index fc36c7e43..14d32ef12 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,4 +1,4 @@ -import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc'; +import { Doc, Opt, DataSym, AclReadonly, AclAugment, AclPrivate, AclEdit, AclSym, DocListCast, AclAdmin, AclSelfEdit } from '../../fields/Doc'; import { Touchable } from './Touchable'; import { computed, action, observable } from 'mobx'; import { Cast, BoolCast, ScriptCast } from '../../fields/Types'; @@ -131,7 +131,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); const indocs = doc instanceof Doc ? [doc] : doc; - const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); + const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin); if (docs.length) { setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin Doc.SetInPlace(doc, "isPushpin", undefined, true); @@ -199,7 +199,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T }); } - if (effectiveAcl === AclAddonly) { + if (effectiveAcl === AclAugment) { added.map(doc => { if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc); doc.context = this.props.Document; @@ -219,7 +219,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc); }); const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; - if (annoDocs) annoDocs.push(...added); + if (annoDocs instanceof List) annoDocs.push(...added); else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); targetDataDoc[(annotationKey ?? this.annotationKey) + "-lastModified"] = new DateField(new Date(Date.now())); } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index df1e6899d..5f09a322c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -355,7 +355,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-button"> <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> - {(DocumentLinksButton.StartLink || Doc.UserDoc()["documentLinksButton-fullMenu"]) && DocumentLinksButton.StartLink != doc ? <div className="documentButtonBar-button"> + {(DocumentLinksButton.StartLink || Doc.UserDoc()["documentLinksButton-fullMenu"]) && DocumentLinksButton.StartLink !== doc ? <div className="documentButtonBar-button"> <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> : (null)} {/*!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index d24ab974c..118d2e7c7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -27,6 +27,7 @@ import { LightboxView } from './LightboxView'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { DateField } from '../../fields/DateField'; @observer export class DocumentDecorations extends React.Component<{ boundsLeft: number, boundsTop: number }, { value: string }> { @@ -367,6 +368,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b dW && (doc._width = actualdW); dH && (doc._autoHeight = false); } + doc._lastModified = new DateField(); } const val = this._dragHeights.get(docView.layoutDoc); if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 6a4f55bef..bbf21f22c 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -726,39 +726,36 @@ export class GestureOverlay extends Touchable { break; case "circle": - + // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%, + // making the curves indistinguishable from a circle. + // Source: https://spencermortensen.com/articles/bezier-circle/ + const c = 0.551915024494; const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); - if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) { - for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - this._points.push({ X: x, Y: y }); - } - for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - const newX = centerX - (x - centerX); - this._points.push({ X: newX, Y: y }); - } - this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) }); - this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) - 1 }); + // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve. + this._points.push({ X: centerX - radius, Y: centerY }); + this._points.push({ X: centerX - radius, Y: centerY + (c * radius) }); + this._points.push({ X: centerX - (c * radius), Y: centerY + radius }); + this._points.push({ X: centerX, Y: centerY + radius }); + + this._points.push({ X: centerX, Y: centerY + radius }); + this._points.push({ X: centerX + (c * radius), Y: centerY + radius }); + this._points.push({ X: centerX + radius, Y: centerY + (c * radius) }); + this._points.push({ X: centerX + radius, Y: centerY }); + + this._points.push({ X: centerX + radius, Y: centerY }); + this._points.push({ X: centerX + radius, Y: centerY - (c * radius) }); + this._points.push({ X: centerX + (c * radius), Y: centerY - radius }); + this._points.push({ X: centerX, Y: centerY - radius }); + + this._points.push({ X: centerX, Y: centerY - radius }); + this._points.push({ X: centerX - (c * radius), Y: centerY - radius }); + this._points.push({ X: centerX - radius, Y: centerY - (c * radius) }); + this._points.push({ X: centerX - radius, Y: centerY }); - - } else { - for (var x = Math.min(left, right); x < Math.max(left, right); x++) { - const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; - this._points.push({ X: x, Y: y }); - } - for (var x = Math.max(left, right); x > Math.min(left, right); x--) { - const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; - const newY = centerY - (y - centerY); - this._points.push({ X: x, Y: newY }); - } - this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY }); - this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY - 1 }); - - } break; + case "line": if (Math.abs(firstx - lastx) < 20) { lastx = firstx; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 76eb4c142..0127d3080 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -27,7 +27,6 @@ import { LightboxView } from "./LightboxView"; import { MainView } from "./MainView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; import { AnchorMenu } from "./pdf/AnchorMenu"; -import { SearchBox } from "./search/SearchBox"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { SettingsManager } from "../util/SettingsManager"; @@ -223,8 +222,11 @@ export class KeyManager { PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); break; case "f": - SearchBox.Instance._searchFullDB = "My Stuff"; - SearchBox.Instance.enter(undefined); + const searchBtn = Doc.UserDoc().searchBtn as Doc; + + if (searchBtn) { + MainView.Instance.selectMenu(searchBtn); + } break; case "o": const target = SelectionManager.Views()[0]; diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index f1eb4b9db..0b24c3c32 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -11,10 +11,12 @@ import { listSpec } from "../../fields/Schema"; import { List } from "../../fields/List"; import { Cast } from "../../fields/Types"; import { Colors } from "./global/globalEnums"; +import { GestureOverlay } from "./GestureOverlay"; export interface InkHandlesProps { inkDoc: Doc; data: InkData; + shape?: string; format: number[]; ScreenToLocalTransform: () => Transform; } @@ -68,6 +70,7 @@ export class InkHandles extends React.Component<InkHandlesProps> { // Accessing the current ink's data and extracting all handle points and handle lines. const data = this.props.data; + const shape = this.props.shape; const handlePoints: HandlePoint[] = []; const handleLines: HandleLine[] = []; if (data.length >= 4) { diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 76ca5b5ec..d527b2a05 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -266,8 +266,13 @@ export class InkStrokeProperties { this.applyFunction((doc: Doc, ink: InkData) => { const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); if (brokenIndices) { - brokenIndices.splice(brokenIndices.indexOf(controlIndex), 1); - doc.brokenInkIndices = brokenIndices; + const newBrokenIndices = new List; + brokenIndices.forEach(brokenIndex => { + if (brokenIndex !== controlIndex) { + newBrokenIndices.push(brokenIndex); + } + }); + doc.brokenInkIndices = newBrokenIndices; const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]]; const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI); const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint); @@ -283,7 +288,7 @@ export class InkStrokeProperties { */ @action rotatePoint = (target: PointData, origin: PointData, angle: number) => { - let rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y }; + const rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y }; const newX = Math.cos(angle) * rotatedTarget.X - Math.sin(angle) * rotatedTarget.Y; const newY = Math.sin(angle) * rotatedTarget.X + Math.cos(angle) * rotatedTarget.Y; rotatedTarget.X = newX + origin.X; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 63cefbf67..5fc159f14 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -20,6 +20,7 @@ import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InkControls } from "./InkControls"; import { InkHandles } from "./InkHandles"; import { Colors } from "./global/globalEnums"; +import { GestureOverlay } from "./GestureOverlay"; type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -78,6 +79,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume TraceMobx(); this.toggleControlButton(); // Extracting the ink data and formatting information of the current ink stroke. + // console.log(InkingStroke.InkShape); + const InkShape = GestureOverlay.Instance.InkShape; const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; const inkDoc: Doc = this.layoutDoc; const strokeWidth = Number(this.layoutDoc.strokeWidth); @@ -100,10 +103,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false); // Thin blue line indicating that the current ink stroke is selected. - const selectedLine = InteractionUtils.CreatePolyline(data, left - strokeWidth / 3, top - strokeWidth / 3, Colors.MEDIUM_BLUE, strokeWidth / 6, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false); // Invisible polygonal line that enables the ink to be selected by the user. - const clickableLine = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier), + const clickableLine = InteractionUtils.CreatePolyline(data, left, top, "transparent", strokeWidth, strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true); // Set of points rendered upon the ink that can be added if a user clicks on one. const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), @@ -142,6 +145,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume <InkHandles inkDoc={inkDoc} data={data} + shape={InkShape} format={[left, top, scaleX, scaleY, strokeWidth]} ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> </> : ""} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 005e46836..6a388c5b4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -13,7 +13,7 @@ import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { BoolCast, PromiseValue, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocUtils } from '../documents/Documents'; @@ -29,28 +29,27 @@ import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; -import { undoBatch, UndoManager } from '../util/UndoManager'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; import { CollectionLinearView } from './collections/CollectionLinearView'; import { CollectionMenu } from './collections/CollectionMenu'; import { CollectionViewType } from './collections/CollectionView'; +import "./collections/TreeView.scss"; import { ContextMenu } from './ContextMenu'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import { GestureOverlay } from './GestureOverlay'; import { MENU_PANEL_WIDTH, SEARCH_PANEL_HEIGHT } from './global/globalCssVariables.scss'; +import { Colors } from './global/globalEnums'; import { KeyManager } from './GlobalKeyHandler'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; -import "./collections/TreeView.scss"; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView, DocumentViewProps, DocAfterFocusFunc } from './nodes/DocumentView'; -import { FieldViewProps } from './nodes/FieldView'; +import { DocumentView } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; @@ -61,10 +60,8 @@ import { OverlayView } from './OverlayView'; import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; -import { SearchBox } from './search/SearchBox'; -import { DefaultStyleProvider, DashboardStyleProvider, StyleProp } from './StyleProvider'; +import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider'; import { TopBar } from './topbar/TopBar'; -import { Colors } from './global/globalEnums'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -107,7 +104,9 @@ export class MainView extends React.Component { new InkStrokeProperties(); this._sidebarContent.proto = undefined; if (!MainView.Live) { - DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "showSidebar", "_sidebarWidthPercent", "_width", "_height", "width", "height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeHidden", "nativeWidth", "_nativeWidth"]); // can play with these fields on someone else's + DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition", + "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap", + "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's } DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); @@ -182,12 +181,6 @@ export class MainView extends React.Component { const targets = document.elementsFromPoint(e.x, e.y); if (targets.length) { const targClass = targets[0].className.toString(); - // if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) { - // const check = targets.some((thing) => - // (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || - // thing.className === "collectionSchema-header-menuOptions")); - // !check && SearchBox.Instance.resetSearch(true); - // } !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu(); !["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu(); } @@ -394,10 +387,6 @@ export class MainView extends React.Component { case "Settings": SettingsManager.Instance.open(); break; - case "Catalog": - SearchBox.Instance._searchFullDB = "My Stuff"; - SearchBox.Instance.enter(undefined); - break; case "Help": break; default: @@ -537,7 +526,7 @@ export class MainView extends React.Component { </svg>; } - @computed get search() { + @computed get topbar() { TraceMobx(); return <div className="mainView-topbar"> <TopBar /> @@ -591,7 +580,7 @@ export class MainView extends React.Component { <GroupManager /> <GoogleAuthenticationManager /> <DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} /> - {this.search} + {this.topbar} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)} @@ -609,7 +598,7 @@ export class MainView extends React.Component { {this.snapLines} <div className="mainView-webRef" ref={this.makeWebRef} /> <LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> - </div>); + </div >); } makeWebRef = (ele: HTMLDivElement) => { diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 805cda95c..a3a3bce56 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -1,6 +1,6 @@ import { action, observable, ObservableMap, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc"; +import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt, AclSelfEdit } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; import { NumCast } from "../../fields/Types"; @@ -156,7 +156,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => { // creates annotation documents for current highlights const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); + const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc); return annotationDoc as Doc ?? undefined; } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 8136edf04..de437e1df 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -5,7 +5,7 @@ import { intersection } from "lodash"; import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; +import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym, AclSelfEdit } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; import { ComputedField } from "../../fields/ScriptField"; @@ -342,12 +342,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => { - return ( - <option key={permission} value={permission}> - {permission === SharingPermissions.Add ? "Can Augment" : permission} - </option>); - })} + {dropdownValues.filter(permission => + !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission => + <option key={permission} value={permission}> {permission} </option>)} </select>; } @@ -402,7 +399,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { [AclUnset, "None"], [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], - [AclAddonly, SharingPermissions.Add], + [AclAugment, SharingPermissions.Augment], + [AclSelfEdit, SharingPermissions.SelfEdit], [AclEdit, SharingPermissions.Edit], [AclAdmin, SharingPermissions.Admin] ]); diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index f1d168a22..1f9763d18 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -24,6 +24,7 @@ interface ExtraProps { rootDoc: Doc; dataDoc: Doc; showSidebar: boolean; + nativeWidth: number; whenChildContentsActiveChanged: (isActive: boolean) => void; ScreenToLocalTransform: () => Transform; sidebarAddDocument: (doc: (Doc | Doc[]), suffix: string) => boolean; @@ -76,7 +77,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { get sidebarKey() { return this.props.fieldKey + "-sidebar"; } filtersHeight = () => 38; screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.props.dataDoc), 0).scale(this.props.scaling?.() || 1); - panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth); + panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth); panelHeight = () => this.props.PanelHeight() - this.filtersHeight(); addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey); moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index c0d39b2a2..cae08e1f4 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -170,7 +170,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const aliasDocList = DocListCast(alias["data-all"]); // if aliasDocList contains the alias, don't do anything // otherwise add the original or an alias depending on whether the doc you're looking at is the current doc or a different alias - !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", alias !== instance.props.Document ? Doc.MakeAlias(document) : document); + !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", document);//alias !== instance.props.Document ? Doc.MakeAlias(document) : document); }); } const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 380f82813..1efea96be 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -556,7 +556,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { style={{ overflow: this.props.scrollOverflow === true ? "scroll" : undefined, backgroundColor: "white", pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined, - width: name === "collectionSchemaView-searchContainer" ? "auto" : this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", + width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", }} > <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a9b5ce465..2f07c0241 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -80,8 +80,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: get childLayoutPairs(): { layout: Doc; data: Doc; }[] { const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc)). - filter(pair => { // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys - return pair.layout && /*!pair.layout.hidden &&*/ (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length)); + filter(pair => { // filter out any documents that have a proto that we don't have permissions to + return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate)); }); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } @@ -93,7 +93,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: childDocFilters = () => [...this.props.docFilters(), ...this.collectionFilters()]; childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()]; IsFiltered = () => this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : - this.props.docFilters().length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined; + this.props.docFilters().length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs); @computed.struct get childDocs() { TraceMobx(); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 3eece0086..c0553ca60 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,10 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, reaction, IReactionDisposer, observable } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { Document } from '../../../fields/Schema'; +import { Document, listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; @@ -215,6 +215,11 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll /> </div>; } + childContextMenuItems = () => { + const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); + const filterScripts = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); + return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: filterScripts[i], label })); + } @computed get treeViewElements() { TraceMobx(); const dropAction = StrCast(this.doc.childDropAction) as dropActionType; @@ -247,7 +252,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.whenChildContentsActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null), this.observerHeight, - this.unobserveHeight); + this.unobserveHeight, + this.childContextMenuItems() + ); } @computed get titleBar() { const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2718cbbbf..a82128e47 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -70,6 +70,7 @@ export interface CollectionViewProps extends FieldViewProps { childDocumentsActive?: () => boolean;// whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode) childFitWidth?: () => boolean; childOpacity?: () => number; + childContextMenuItems?: () => { script: ScriptField, label: string }[]; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children childHideDecorationTitle?: () => boolean; childLayoutTemplate?: () => (Doc | undefined);// specify a layout Doc template to use for children of the collection @@ -186,15 +187,20 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer, icon: "project-diagram" }); - optionItems.push({ - description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram" - }); - optionItems.push({ - description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram" - }); - optionItems.push({ - description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram" - }); + if (!Doc.UserDoc().noviceMode) { + optionItems.push({ + description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram" + }); + optionItems.push({ + description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram" + }); + optionItems.push({ + description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram" + }); + } + if (this.Document._viewType === CollectionViewType.Docking) { + optionItems.push({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "project-diagram" }); + } !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); @@ -236,7 +242,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab * Shows the filter icon if it's a user-created collection which isn't a dashboard and has some docFilters applied on it or on the current dashboard. */ @computed get showFilterIcon() { - return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && this._subView?.IsFiltered() + return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && this._subView?.IsFiltered(); } @observable _subView: any = undefined; @@ -245,10 +251,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab TraceMobx(); const props: SubCollectionViewProps = { ...this.props, - SetSubView: action((subView: any) => { - console.log("Setting sub", subView); - return this._subView = subView - }), + SetSubView: action((subView: any) => this._subView = subView), addDocument: this.addDocument, moveDocument: this.moveDocument, removeDocument: this.removeDocument, diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 623e0f58d..1969d728c 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -171,10 +171,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { //attach the selection doc buttons menu to the drag handle const stack: HTMLDivElement = tab.contentItem.parent; const header: HTMLDivElement = tab; - console.log("Stack: " + stack.id, stack.className) stack.onscroll = action((e: any) => { - console.log('scrolling...') - }) + console.log('scrolling...'); + }); const moreInfoDrag = document.createElement("div"); moreInfoDrag.className = "lm_iconWrap"; tab._disposers.buttonDisposer = reaction(() => this.view, view => diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index e33c39d20..0598be49b 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; @@ -54,6 +54,7 @@ export interface TreeViewProps { indentDocument?: (editTitle: boolean) => void; outdentDocument?: (editTitle: boolean) => void; ScreenToLocalTransform: () => Transform; + contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[]; dontRegisterView?: boolean; styleProvider?: StyleProviderFunc | undefined; treeViewHideHeaderFields: () => boolean; @@ -336,7 +337,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.props.dropAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged, - this.props.dontRegisterView, emptyFunction, emptyFunction); + this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems()); } else { contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} @@ -418,7 +419,7 @@ export class TreeView extends React.Component<TreeViewProps> { StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged, - this.props.dontRegisterView, emptyFunction, emptyFunction)} + this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul key={this.doc[Id] + this.doc.title}> @@ -507,11 +508,19 @@ export class TreeView extends React.Component<TreeViewProps> { } contextMenuItems = () => { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" }; - return this.doc.isFolder ? [makeFolder] : + const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }; + const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }; + return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [makeFolder] : Doc.IsSystem(this.doc) ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? - [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] : - [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }]; + [openAlias, makeFolder] : + this.doc.viewType === CollectionViewType.Docking ? [] : + [openAlias, focusDoc])]; + } + childContextMenuItems = () => { + const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); + const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); + return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], label })); } onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); @@ -817,7 +826,8 @@ export class TreeView extends React.Component<TreeViewProps> { whenChildContentsActiveChanged: (isActive: boolean) => void, dontRegisterView: boolean | undefined, observerHeight: (ref: any) => void, - unobserveHeight: (ref: any) => void + unobserveHeight: (ref: any) => void, + contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[]) ) { const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -882,6 +892,7 @@ export class TreeView extends React.Component<TreeViewProps> { parentTreeView={parentTreeView} observeHeight={observerHeight} unobserveHeight={unobserveHeight} + contextMenuItems={contextMenuItems} />; }); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ba6222605..fa7e75202 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -49,6 +49,7 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { DocumentType } from "../../../documents/DocumentTypes"; +import { DateField } from "../../../../fields/DateField"; export const panZoomSchema = createSchema({ _panX: "number", @@ -257,6 +258,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; } + d._lastModified = new DateField(); const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 846d28214..19da7ea00 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,6 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc"; +import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; @@ -298,7 +298,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downX = x; this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) { + if ([AclAdmin, AclEdit, AclAugment].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/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx index b2115b22e..aaa50ba67 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx @@ -424,7 +424,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove"); e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log(""); e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter(); - e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove"); + e.target.checked === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove"); }} checked={bool} /> diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx index de08c327a..abe549072 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -197,6 +197,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> <FontAwesomeIcon icon={sortIcon} size="lg" /> </div> + {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} </div>; return { @@ -561,7 +562,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {this.props.Document._chromeHidden ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} + {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} {!this._showDoc ? (null) : <div className="collectionSchemaView-documentPreview" ref="overlay" style={{ diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 2c4b718f4..df469c53b 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -54,7 +54,6 @@ export class LinkPopup extends React.Component<LinkPopupProps> { @action onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.linkURL = e.target.value; - console.log(this.linkURL) } diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index b63174e54..7648e866e 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -114,7 +114,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } - }; + } })); } @@ -291,7 +291,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp (null) } </div> - ) + ); } render() { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index bea219831..a29f2f662 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -420,7 +420,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps focus = (anchor: Doc, options?: DocFocusOptions) => { LightboxView.SetCookie(StrCast(anchor["cookies-set"])); - // copying over _VIEW fields immediately allows the view type to switch to create the right _componentView + // copying over VIEW fields immediately allows the view type to switch to create the right _componentView Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => { this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]); }); @@ -664,7 +664,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); - Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => + StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); this.props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index fc8fa4eef..23236cf20 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -9,7 +9,7 @@ import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast, BoolCast } from '../../../fields/Types'; import { PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; -import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils'; +import { Utils, setupMoveUpEvents, emptyFunction, returnOne } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch } from '../../util/UndoManager'; @@ -31,6 +31,7 @@ const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @observer export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } + public static openSidebarWidth = 250; private _searchString: string = ""; private _initialScrollTarget: Opt<Doc>; private _pdfViewer: PDFViewer | undefined; @@ -146,11 +147,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return false; }, emptyFunction, () => this.toggleSidebar()); } + @observable _previewNativeWidth: Opt<number> = undefined; + @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - if (preview) this._showSidebar = true; + if (preview) { + this._previewNativeWidth = nativeWidth * ratio; + this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this._showSidebar = true; + } else { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; @@ -208,7 +215,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </button> </div>; } - sidebarWidth = () => !this.SidebarShown ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); + sidebarWidth = () => !this.SidebarShown ? 0 : + this._previewWidth ? PDFBox.openSidebarWidth : + (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth) specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; @@ -232,38 +241,51 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _showSidebar = false; @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } - + contentScaling = () => { + return 1; + } @computed get renderPdfView() { TraceMobx(); + const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; + const scale = previewScale * (this.props.scaling?.() || 1); return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> <div className="pdfBox-background" /> - <PDFViewer {...this.props} - rootDoc={this.rootDoc} - layoutDoc={this.layoutDoc} - dataDoc={this.dataDoc} - pdf={this._pdf!} - url={this.pdfUrl!.url.pathname} - isContentActive={this.isContentActive} - anchorMenuClick={this.anchorMenuClick} - loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} - setPdfViewer={this.setPdfViewer} - addDocument={this.addDocument} - moveDocument={this.moveDocument} - removeDocument={this.removeDocument} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - startupLive={true} - ContentScaling={this.props.scaling} - sidebarWidth={this.sidebarWidth} - /> + <div style={{ + width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`, + height: `${100 / scale}%`, + transform: `scale(${scale})`, + position: "absolute", + transformOrigin: "top left", + top: 0 + }}> + <PDFViewer {...this.props} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + pdf={this._pdf!} + url={this.pdfUrl!.url.pathname} + isContentActive={this.isContentActive} + anchorMenuClick={this.anchorMenuClick} + loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} + setPdfViewer={this.setPdfViewer} + addDocument={this.addDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + startupLive={true} + ContentScaling={returnOne} + /> + </div> <SidebarAnnos ref={this._sidebarRef} {...this.props} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} showSidebar={this.SidebarShown} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} sidebarAddDocument={this.sidebarAddDocument} diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 0e235a62d..68ab3193b 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,11 +1,11 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; // import { Canvas } from '@react-three/fiber'; -import { action, computed, observable, reaction } from "mobx"; +import { action, computed, observable, reaction, trace, runInAction } from "mobx"; import { observer } from "mobx-react"; // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three'; import { DateField } from "../../../fields/DateField"; -import { Doc, WidthSym } from "../../../fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; @@ -218,9 +218,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl // } return (null); } - toggleRecording = action(async () => { - this._screenCapture = !this._screenCapture; - if (this._screenCapture) { + toggleRecording = async () => { + if (!this._screenCapture) { this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); const aud_chunks: any = []; this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); @@ -249,18 +248,24 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; this._audioRec.start(); this._videoRec.start(); - this.dataDoc.mediaState = "recording"; + runInAction(() => { + this._screenCapture = true; + this.dataDoc.mediaState = "recording"; + }); DocUtils.ActiveRecordings.push(this); } else { this._audioRec?.stop(); this._videoRec?.stop(); - this.dataDoc.mediaState = "paused"; + runInAction(() => { + this._screenCapture = false; + this.dataDoc.mediaState = "paused"; + }); const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); CaptureManager.Instance.open(this.rootDoc); } - }); + }; setupDictation = () => { if (this.dataDoc[this.fieldKey + "-dictation"]) return; @@ -275,7 +280,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this.dataDoc[this.fieldKey + "-dictation"] = dictationText; } contentFunc = () => [this.threed, this.content]; - videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth(); + videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth(); formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { TraceMobx(); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index bb2c09fdf..f3a4a46de 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -43,6 +43,7 @@ const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, WebDocument>(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } + public static openSidebarWidth = 250; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -51,6 +52,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _keyInput = React.createRef<HTMLInputElement>(); private _initialScroll: Opt<number>; private _sidebarRef = React.createRef<SidebarAnnos>(); + @observable private _urlHash: string = ""; @observable private _scrollTimer: any; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -67,7 +69,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps constructor(props: any) { super(props); - if (this.webField) { + if (true) {// his.webField) { Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); } @@ -81,9 +83,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps runInAction(() => { this._url = this.webField?.toString() || ""; - this._annotationKey = "annotations-" + WebBox.urlHash(this._url); + this._urlHash = WebBox.urlHash(this._url) + ""; + this._annotationKey = this._urlHash + "-annotations"; // bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created) - this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-annotations-"+urlHash(this["${this.fieldKey}"]?.url?.toString()))`); + this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`); + this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`); }); this._disposers.selection = reaction(() => this.props.isSelected(), @@ -160,6 +164,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected scrollFocus = (doc: Doc, smooth: boolean) => { + if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth); if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) { this.toggleSidebar(!smooth); } @@ -181,12 +186,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps getAnchor = () => { const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? - Docs.Create.TextanchorDocument({ + Docs.Create.WebanchorDocument(this._url, { title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), annotationOn: this.rootDoc, - y: NumCast(this.layoutDoc._scrollTop), + y: NumCast(this.layoutDoc._scrollTop) }); - this.addDocument(anchor); + this.addDocumentWrapper(anchor); return anchor; } @@ -298,9 +303,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (future.length) { - history.push(this._url); + this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); - this._annotationKey = "annotations-" + WebBox.urlHash(this._url); + this._urlHash = WebBox.urlHash(this._url) + ""; + this._annotationKey = this._urlHash + "-annotations"; return true; } return false; @@ -312,9 +318,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (history.length) { if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); - else future.push(this._url); + else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!)); - this._annotationKey = "annotations-" + WebBox.urlHash(this._url); + this._urlHash = WebBox.urlHash(this._url) + ""; + this._annotationKey = this._urlHash + "-annotations"; return true; } return false; @@ -325,25 +332,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } @action - submitURL = (newUrl?: string) => { + submitURL = (newUrl?: string, preview?: boolean) => { if (!newUrl) return; if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; try { const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string")); const url = this.webField?.toString(); - if (url) { + if (url && !preview) { if (history === undefined) { this.dataDoc[this.fieldKey + "-history"] = new List<string>([url]); } else { - history.push(url); + this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, url]); } this.layoutDoc._scrollTop = 0; future && (future.length = 0); } this._url = newUrl; - this._annotationKey = "annotations-" + WebBox.urlHash(this._url); - this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); + this._urlHash = WebBox.urlHash(this._url) + ""; + this._annotationKey = this._urlHash + "-annotations"; + if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); } catch (e) { console.log("WebBox URL error:" + this._url); } @@ -421,7 +429,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (field instanceof HtmlField) { view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; + const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url; view = <iframe className="webBox-iframe" enable-annotation={"true"} style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} @@ -436,9 +444,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return view; } + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { + (doc instanceof Doc ? [doc] : doc).forEach(doc => doc.webUrl = this._url); + return this.addDocument(doc, annotationKey); + } + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); - return this.addDocument(doc, sidebarKey); + return this.addDocumentWrapper(doc, sidebarKey); } sidebarBtnDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, (e, down, delta) => { @@ -454,18 +467,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return false; }, emptyFunction, () => this.toggleSidebar()); } + @observable _previewNativeWidth: Opt<number> = undefined; + @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); - if (preview) this._showSidebar; + if (preview) { + this._previewNativeWidth = nativeWidth * ratio; + this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this._showSidebar = true; + } else { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } }); - sidebarWidth = () => !this.SidebarShown ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); + sidebarWidth = () => !this.SidebarShown ? 0 : + this._previewWidth ? WebBox.openSidebarWidth : + (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / + NumCast(this.layoutDoc.nativeWidth) @computed get content() { return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.isContentActive() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} @@ -493,29 +515,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; render() { - const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; - const scale = this.props.scaling?.() || 1; + const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined; + const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; + const scale = previewScale * (this.props.scaling?.() || 1); return ( <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.isContentActive() ? "all" : this.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} > - <div className={`webBox-container`} - style={{ pointerEvents: inactiveLayer ? "none" : undefined }} - onContextMenu={this.specificContextMenu}> + <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}> <base target="_blank" /> <div className={"webBox-outerContent"} ref={this._outerRef} style={{ - width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale}px)`, + width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`, height: `${100 / scale}%`, transform: `scale(${scale})`, - pointerEvents: inactiveLayer ? "none" : undefined + pointerEvents }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }} // block wheel events from propagating since they're handled by the iframe onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown} > - <div className={"webBox-innerContent"} style={{ - height: NumCast(this.scrollHeight, 50), - pointerEvents: inactiveLayer ? "none" : undefined - }}> + <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents }}> {this.content} <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} @@ -548,7 +566,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps anchorMenuClick={this.anchorMenuClick} scrollTop={0} down={this._marqueeing} scaling={returnOne} - addDocument={this.addDocument} + addDocument={this.addDocumentWrapper} docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} @@ -557,10 +575,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </div > <SidebarAnnos ref={this._sidebarRef} {...this.props} - fieldKey={this.fieldKey} + fieldKey={this.fieldKey + "-" + this._urlHash} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} showSidebar={this.SidebarShown} sidebarAddDocument={this.sidebarAddDocument} moveDocument={this.moveDocument} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d308345ec..b0b8ce75d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,7 +11,7 @@ import { ReplaceStep } from 'prosemirror-transform'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc"; +import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; @@ -215,7 +215,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp AnchorMenu.Instance.Status = "marquee"; AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch); - console.log("highlight") return undefined; }); /** @@ -254,7 +253,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const removeSelection = (json: string | undefined) => json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || effectiveAcl === AclSelfEdit) { const accumTags = [] as string[]; state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) { @@ -432,6 +431,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.ProseRef = ele; this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc)); + // if (this.autoHeight) this.tryUpdateScrollHeight(); } @undoBatch @@ -535,7 +535,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); - setTimeout(() => this.updateHighlights()); + setTimeout(this.updateHighlights); } if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); @@ -882,7 +882,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } }, ); - if (this._recording) setTimeout(() => this.recordDictation()); + if (this._recording) setTimeout(this.recordDictation); } var quickScroll: string | undefined = ""; this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop), @@ -1402,6 +1402,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._rules!.EnteringStyle = false; } e.stopPropagation(); + for (var i = state.selection.from; i < state.selection.to; i++) { + const node = state.doc.resolve(i); + if (node?.marks?.().some(mark => mark.type === schema.marks.user_mark && + mark.attrs.userid !== Doc.CurrentUserEmail) && + [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) { + e.preventDefault(); + } + } switch (e.key) { case "Escape": this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); @@ -1431,16 +1439,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } - tryUpdateScrollHeight() { + tryUpdateScrollHeight = () => { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const proseHeight = !this.ProseRef ? 0 : Array.from(this.ProseRef.children[0].children).reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); - if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; - if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { - setScrollHeight(); - } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; + if (children) { + var proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); + var scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; + if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + setScrollHeight(); + setTimeout(() => { + proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); + scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + scrollHeight && setScrollHeight(); + }, 10); + } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + } } } } @@ -1480,6 +1496,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} showSidebar={this.SidebarShown} PanelWidth={this.sidebarWidth} setHeight={this.setSidebarHeight} diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index d5c77786c..eff400a98 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -7,13 +7,14 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { SelectionManager } from "../../../util/SelectionManager"; import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; -import { Doc, DataSym, DocListCast } from "../../../../fields/Doc"; +import { Doc, DataSym, DocListCast, AclAugment, AclSelfEdit } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; import { Id } from "../../../../fields/FieldSymbols"; import { Docs } from "../../../documents/Documents"; import { Utils } from "../../../../Utils"; import { listSpec } from "../../../../fields/Schema"; import { List } from "../../../../fields/List"; +import { GetEffectiveAcl } from "../../../../fields/util"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -70,25 +71,42 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey return false; }; + const canEdit = (state: any) => { + switch (GetEffectiveAcl(props.Document)) { + case AclAugment: return false; + case AclSelfEdit: + for (var i = state.selection.from; i < state.selection.to; i++) { + const marks = state.doc.resolve(i)?.marks?.(); + if (marks?.some((mark: any) => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) { + return false; + } + } + break; + } + return true; + } + + const toggleEditableMark = (mark: any) => (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && toggleMark(mark)(state, dispatch); + //History commands bind("Mod-z", undo); bind("Shift-Mod-z", redo); !mac && bind("Mod-y", redo); //Commands to modify Mark - bind("Mod-b", toggleMark(schema.marks.strong)); - bind("Mod-B", toggleMark(schema.marks.strong)); + bind("Mod-b", toggleEditableMark(schema.marks.strong)); + bind("Mod-B", toggleEditableMark(schema.marks.strong)); - bind("Mod-e", toggleMark(schema.marks.em)); - bind("Mod-E", toggleMark(schema.marks.em)); + bind("Mod-e", toggleEditableMark(schema.marks.em)); + bind("Mod-E", toggleEditableMark(schema.marks.em)); - bind("Mod-*", toggleMark(schema.marks.code)); + bind("Mod-*", toggleEditableMark(schema.marks.code)); - bind("Mod-u", toggleMark(schema.marks.underline)); - bind("Mod-U", toggleMark(schema.marks.underline)); + bind("Mod-u", toggleEditableMark(schema.marks.underline)); + bind("Mod-U", toggleEditableMark(schema.marks.underline)); //Commands for lists - bind("Ctrl-i", wrapInList(schema.nodes.ordered_list)); + bind("Ctrl-i", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state, dispatch as any)); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { /// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab); @@ -96,6 +114,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if (!props.LayoutTemplateString) return addTextBox(false, true); return true; } + if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -121,6 +140,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { /// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab); if (props.Document._singleLine) return true; + if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => { @@ -140,24 +160,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); //Commands to modify BlockType - bind("Ctrl->", wrapIn(schema.nodes.blockquote)); - bind("Alt-\\", setBlockType(schema.nodes.paragraph)); - bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block)); + bind("Ctrl->", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit((state) && wrapIn(schema.nodes.blockquote)(state, dispatch as any))); + bind("Alt-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state, dispatch as any)); + bind("Shift-Ctrl-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state, dispatch as any)); - bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() }))); - }); + bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() })))); for (let i = 1; i <= 6; i++) { - bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i })); + bind("Shift-Ctrl-" + i, (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state, dispatch as any)); } //Command to create a horizontal break line const hr = schema.nodes.horizontal_rule; - bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); - return true; - }); + bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView())); //Command to unselect all bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { @@ -173,13 +188,15 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }; //Command to create a text document to the right of the selected textbox - bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => addTextBox(false, true)); + bind("Alt-Enter", () => addTextBox(false, true)); //Command to create a text document to the bottom of the selected textbox - bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => addTextBox(true, true)); + bind("Ctrl-Enter", () => addTextBox(true, true)); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + if (!canEdit(state)) return true; + if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => { dispatch(updateBullets(tx, schema)); })) { @@ -200,6 +217,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { if (addTextBox(true, false)) return true; + + if (!canEdit(state)) return true; + const trange = state.selection.$from.blockRange(state.selection.$to); const path = (state.selection.$from as any).path; const depth = trange ? liftTarget(trange) : undefined; @@ -238,18 +258,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a blank space bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; }); - bind("Alt-ArrowUp", joinUp); - bind("Alt-ArrowDown", joinDown); - bind("Mod-BracketLeft", lift); + bind("Alt-ArrowUp", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinUp(state, dispatch as any)); + bind("Alt-ArrowDown", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinDown(state, dispatch as any)); + bind("Mod-BracketLeft", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && lift(state, dispatch as any)); const cmd = chainCommands(exitCode, (state, dispatch) => { if (dispatch) { - dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView()); + canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView()); return true; } return false; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index e8c7a4ab0..e7911e8f8 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -46,7 +46,6 @@ interface IViewerProps extends FieldViewProps { loaded?: (nw: number, nh: number, np: number) => void; setPdfViewer: (view: PDFViewer) => void; ContentScaling?: () => number; - sidebarWidth: () => number; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); } @@ -550,7 +549,6 @@ export class PDFViewer extends React.Component<IViewerProps> { onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ overflowX: this._zoomed !== 1 ? "scroll" : undefined, - width: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeWidth(this.props.Document) - this.props.sidebarWidth() / this.contentScaling : `calc(${100 / this.contentScaling}% - ${this.props.sidebarWidth() / this.contentScaling}px)`, height: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeHeight(this.props.Document) : `${100 / this.contentScaling}%`, transform: `scale(${this.contentScaling})` }} > diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 6a2fe6f19..2586ef2ee 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -2,141 +2,109 @@ @import "./NaviconButton.scss"; .searchBox-container { - display: flex; - flex-direction: column; width: 100%; height: 100%; - position: relative; font-size: 10px; line-height: 1; - overflow-y: auto; - overflow-x: visible; - background: lightgrey; - overflow: visible; + background: none; z-index: 1000; + padding: 0px; + cursor: default; .searchBox-bar { - height: $searchpanel-height; + width: 100%; + height: 35px; display: flex; justify-content: center; align-items: center; - background-color: $dark-gray; + background-color: none; + padding: 5px; - .searchBox-lozenges { - position: absolute; - left: 15; - display: flex; - - .searchBox-lozenge-user, - .searchBox-lozenge-dashboard, - .searchBox-lozenge { - height: 18px; - padding: 4px; - margin-right: 5px; - display: flex; - align-items: center; - border: grey 1px solid; - .searchBox-logoff, - .searchBox-dashboards { - border-radius: 3px; - background: olivedrab; - color: white; - display: none; - margin-left: 5px; - padding: 1px 2px 1px 2px; - cursor: pointer; - } - .searchBox-logoff { - background: red; - } - - .searchBox-dashSelect{ - border: none; - background-color: transparent; + .searchBox-type { + display: block; + width: 55px; + outline: none; + padding: 1px 5px 1px 5px; + color: black; + height: 25px; + border: 1px solid black; + border-right: 0px; + } - &:hover { - cursor: pointer; - } - } - } - .searchBox-lozenge-user:hover { - .searchBox-logoff { - display:inline-block; - } - } - .searchBox-lozenge-dashboard:hover { - .searchBox-dashboards { - display:inline-block; - } - } + .searchBox-input { + display: block; + width: calc(100% - 55px); + outline: none; + padding: 1px 5px 1px 5px; + color: black; + height: 25px; + border: 1px solid black; } - .searchBox-query { - position: relative; + } + + .searchBox-results-container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + justify-content: "center"; + + .searchBox-results-count { display: flex; - width: 450; + color: gray; + margin-left: 5px; } - .searchBox-barChild { + + .searchBox-results-scroll-view { + margin-top: 10px; + display: inline-block; + width: 100%; + height: calc(100% - 55px); + overflow-y: scroll; - &.searchBox-collection { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px - } + .searchBox-results-scroll-view-result { + display: inline-block; + vertical-align: middle; + width: 100%; + height: 50px; + cursor: pointer; + font-size: 15px; + padding: 11px; - &.searchBox-input { - margin:5px; - border-radius:20px; - border:$dark-gray; - display: block; - width: 130px; - -webkit-transition: width 0.4s; - transition: width 0.4s; - align-self: stretch; - outline:none; - &:focus { - width: 500px; - outline:none; + &.searchBox-results-scroll-view-result-selected { + background: #999; } - } - &.searchBox-filter { - align-self: stretch; - button{ - transform:none; - &:hover { - transform: none; - } + + .searchBox-result-title { + display: relative; + float: left; + width: calc(100% - 60px); + text-align: left; } - } - &.searchBox-submit { - margin-left: 2px; - margin-right: 2px - } + .searchBox-result-type { + font-size: 12px; + margin-top: 6px; + display: relative; + float: right; + width: 60px; + text-align: right; + color: #222; + } - &.searchBox-close { - color: $white; - max-height: $searchpanel-height; + .searchBox-result-keys { + font-size: 10px; + margin-top: 1px; + display: relative; + float: left; + width: 100%; + text-align: left; + color: #555; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } } - } -} - -.searchBox-results { - display: flex; - flex-direction: column; - top: 300px; - display: flex; - flex-direction: column; - height: 100%; - overflow: visible; - - .no-result { - width: 500px; - background: $light-gray; - padding: 10px; - height: 50px; - text-transform: uppercase; - text-align: left; - font-weight: bold; } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index c72b25040..b07879674 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,601 +1,325 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Field, Opt, DocListCastAsync } from '../../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; -import { Copy, Id } from '../../../fields/FieldSymbols'; -import { List } from '../../../fields/List'; -import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; -import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; -import { Docs } from '../../documents/Documents'; +import { Id } from '../../../fields/FieldSymbols'; +import { createSchema, makeInterface } from '../../../fields/Schema'; +import { StrCast } from '../../../fields/Types'; import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { SetupDrag } from '../../util/DragManager'; -import { SearchUtil } from '../../util/SearchUtil'; -import { Transform } from '../../util/Transform'; +import { DocumentManager } from "../../util/DocumentManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionSchemaView, ColumnType } from "../collections/collectionSchema/CollectionSchemaView"; -import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; -import { undoBatch } from "../../util/UndoManager"; -import { DocServer } from "../../DocServer"; -import { MainView } from "../MainView"; export const searchSchema = createSchema({ Document: Doc }); type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>; const SearchBoxDocument = makeInterface(documentSchema, searchSchema); +/** + * This is the SearchBox component. It represents the search box input and results in + * the search panel on the left side of the screen. + */ @observer export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } public static Instance: SearchBox; - private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; - private _numResultsPerPage = 500; - private _numTotalResults = -1; - private _endIndex = -1; - private _lockPromise?: Promise<void>; - private _resultsSet = new Map<Doc, number>(); private _inputRef = React.createRef<HTMLInputElement>(); - private _maxSearchIndex: number = 0; - private _curRequest?: Promise<any> = undefined; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - - private docsforfilter: Doc[] | undefined = []; - private realTotalResults: number = 0; - private newsearchstring = ""; - private collectionRef = React.createRef<HTMLDivElement>(); - - - @observable _undoBackground: string | undefined = ""; - @observable _icons: string[] = this._allIcons; - @observable _results: [Doc, string[], string[]][] = []; - @observable _visibleElements: JSX.Element[] = []; - @observable _visibleDocuments: Doc[] = []; + + @observable _searchString = ""; + @observable _docTypeString = "all"; + @observable _results: [Doc, string[]][] = []; + @observable _selectedResult: Doc | undefined = undefined; @observable _deletedDocsStatus: boolean = false; @observable _onlyAliases: boolean = true; - @observable _searchbarOpen = false; - @observable _searchFullDB = "DB"; // "DB" means searh the entire database. "My Stuff" adds a flag that selects only documents that the current user has authored - @observable _noResults = ""; - @observable _pageStart = 0; - @observable open = false; - @observable children = 0; - @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); } + /** + * This is the constructor for the SearchBox class. + */ constructor(props: any) { super(props); SearchBox.Instance = this; } + /** + * This method is called when the SearchBox component is first mounted. When the user opens + * the search panel, the search input box is automatically selected. This allows the user to + * type in the search input box immediately, without needing clicking on it first. + */ componentDidMount = action(() => { if (this._inputRef.current) { this._inputRef.current.focus(); } - this._disposers.filters = reaction(() => this.props.Document._docFilters, - (filters: any) => this.setSearchFilter(this.currentSelectedCollection, !this.filter ? undefined : this.docsforfilter)); }); + /** + * This method is called when the SearchBox component is about to be unmounted. When the user + * closes the search panel, the search and its results are reset. + */ componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); + this.resetSearch(); } - @computed get currentSelectedCollection() { return CollectionDockingView.Instance; } - - onChange = action((e: React.ChangeEvent<HTMLInputElement>) => { - this.newsearchstring = e.target.value; - if (e.target.value === "") { - console.log("Reset start"); - this.docsforfilter = undefined; - this.setSearchFilter(this.currentSelectedCollection, undefined); - this.resetSearch(false); - - this.open = false; - this._results = []; - this._resultsSet.clear(); - this._visibleElements = []; - this._numTotalResults = -1; - this._endIndex = -1; - this._curRequest = undefined; - this._maxSearchIndex = 0; - } + /** + * This method is called when the text in the search input box is modified by the user. The + * _searchString is updated to the new value of the text in the input box and submitSearch + * is called to update the search results accordingly. + * + * (Note: There is no longer a need to press enter to submit a search. Any update to the input + * causes a search to be submitted automatically.) + */ + onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.target.value; + this.submitSearch(); }); - enter = action((e: React.KeyboardEvent | undefined) => { - if (!e || e.key === "Enter") { - this.layoutDoc._searchString = this.newsearchstring; - this._pageStart = 0; - this.open = StrCast(this.layoutDoc._searchString) !== "" || this._searchFullDB !== "DB"; - this.submitSearch(); - } + /** + * This method is called when the option in the select drop-down menu is changed. The + * _docTypeString is updated to the new value of the option in the drop-down menu. This + * is used to filter the results of the search to documents of a specific type. + * + * (Note: This doesn't affect the results array, so there is no need to submit a new + * search here. The results of the search on the _searchString query are simply filtered + * by type directly before rendering them.) + */ + onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => { + this._docTypeString = e.target.value; }); - getFinalQuery(query: string): string { - //alters the query so it looks in the correct fields - //if this is true, then not all of the field boxes are checked - //TODO: data - const initialfilters = Cast(this.props.Document._docFilters, listSpec("string"), []); - - const filters: string[] = []; - - for (const initFilter of initialfilters) { - const fields = initFilter.split(":"); - if (fields[2] !== undefined) { - filters.push(fields[0]); - filters.push(fields[1]); - filters.push(fields[2]); - } - } - - const finalfilters: { [key: string]: string[] } = {}; - - for (let i = 0; i < filters.length; i = i++) { - const fields = filters[i].split(":"); - if (finalfilters[fields[0]] !== undefined) { - finalfilters[fields[0]].push(fields[1]); - } - else { - finalfilters[fields[0]] = [fields[1]]; - } - } - - for (const key in finalfilters) { - const values = finalfilters[key]; - if (values.length === 1) { - const mod = "_t:"; - const newWords: string[] = []; - const oldWords = values[0].split(" "); - oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word)); - query = `(${query}) AND (${newWords.join(" ")})`; - } - else { - for (let i = 0; i < values.length; i++) { - const mod = "_t:"; - const newWords: string[] = []; - const oldWords = values[i].split(" "); - oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word)); - const v = "(" + newWords.join(" ") + ")"; - if (i === 0) { - query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : ""); - } - else query = query + " OR " + v + (i === values.length - 1 ? ")" : ""); - } - } - } - - return query.replace(/-\s+/g, ''); - } - - @action - filterDocsByType(docs: Doc[]) { - const finalDocs: Doc[] = []; - docs.forEach(doc => { - const layoutresult = StrCast(doc.type, "string") as DocumentType; - if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) { - finalDocs.push(doc); - } - }); - return finalDocs; - } + /** + * @param {Doc} doc - doc of the search result that has been clicked on + * + * This method is called when the user clicks on a search result. The _selectedResult is + * updated accordingly and the doc is highlighted with the selectElement method. + */ + onResultClick = action((doc: Doc) => { + this.selectElement(doc); + this._selectedResult = doc; + }); - static async foreachRecursiveDocAsync(docs: Doc[], func: (doc: Doc) => void) { + /** + * @param {Doc[]} docs - docs to be searched through recursively + * @param {number, Doc => void} func - function to be called on each doc + * + * This method iterates through an array of docs and all docs within those docs, calling + * the function func on each doc. + */ + static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) { let newarray: Doc[] = []; + var depth = 0; while (docs.length > 0) { newarray = []; - await Promise.all(docs.filter(d => d).map(async d => { + docs.filter(d => d).forEach(d => { const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); const data = d[annos ? fieldKey + "-annotations" : fieldKey]; - const docs = await DocListCastAsync(data); - docs && newarray.push(...docs); - func(d); - })); + data && newarray.push(...DocListCast(data)); + func(depth, d); + }); docs = newarray; + depth++; } } - static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) { + + /** + * @param {Doc[]} docs - docs to be searched through recursively + * @param {number, Doc => void} func - function to be called on each doc + * + * This method iterates asynchronously through an array of docs and all docs within those + * docs, calling the function func on each doc. + */ + static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) { let newarray: Doc[] = []; + var depth = 0; while (docs.length > 0) { newarray = []; - docs.filter(d => d).forEach(d => { + await Promise.all(docs.filter(d => d).map(async d => { const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); const data = d[annos ? fieldKey + "-annotations" : fieldKey]; - data && newarray.push(...DocListCast(data)); - func(d); - }); + const docs = await DocListCastAsync(data); + docs && newarray.push(...docs); + func(depth, d); + })); docs = newarray; + depth++; } } + /** + * @param {String} type - string representing the type of a doc + * + * This method converts a doc type string of any length to a 3-letter doc type string in + * which the first letter is capitalized. This is used when displaying the type on the + * right side of each search result. + */ + static formatType(type: String): String { + if (type == "pdf") { + return "PDF"; + } + else if (type == "image") { + return "Img"; + } + + return type.charAt(0).toUpperCase() + type.substring(1, 3); + } + + /** + * @param {String} query - search query string + * + * This method searches the CollectionDockingView instance for a certain query and puts + * the matching results in the results array. Docs are considered to be matching results + * when the query is a substring of many different pieces of its metadata (title, text, + * author, etc). + */ @action searchCollection(query: string) { - const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0]; + const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; + const blockedKeys = ["x", "y", "proto", "width", "autoHeight", "acl-Override", "acl-Public", "context", "zIndex", "height", "text-scrollHeight", "text-height", "cloneFieldFilter", "isPrototype", "text-annotations", + "dragFactory-count", "text-noTemplate", "aliases", "system", "layoutKey", "baseProto", "xMargin", "yMargin", "links", "layout", "layout_keyValue", "fitWidth", "viewType", "title-custom", + "panX", "panY", "viewScale"] + const collection = CollectionDockingView.Instance; query = query.toLowerCase(); - if (selectedCollection !== undefined) { - // this._currentSelectedCollection = selectedCollection; - const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); - const found: [Doc, string[], string[]][] = []; - SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => { - const hlights = new Set<string>(); - SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key)); - Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]); + this._results = [] + this._selectedResult = undefined + + if (collection !== undefined) { + const docs = DocListCast(collection.rootDoc[Doc.LayoutFieldKey(collection.rootDoc)]); + const docIDs: String[] = [] + console.log(docs.length) + SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { + const dtype = StrCast(doc.type, "string") as DocumentType; + if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth > 0) { + const hlights = new Set<string>(); + SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key)); + blockedKeys.forEach(key => { + hlights.delete(key); + }) + Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]); + } + docIDs.push(doc[Id]) }); - - this._results = found; - this.docsforfilter = this._results.map(r => r[0]); - this.setSearchFilter(selectedCollection, this.filter && found.length ? this.docsforfilter : undefined); - this._numTotalResults = found.length; - this.realTotalResults = found.length; } - else { - this._noResults = "No collection selected :("; - } - } + /** + * @param {Doc} doc - doc for which keys are returned + * + * This method returns a list of a document doc's keys. + */ static documentKeys(doc: Doc) { const keys: { [key: string]: boolean } = {}; - // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. - // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be - // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. - // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu - // is displayed (unlikely) it won't show up until something else changes. - //TODO Types Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)); return Array.from(Object.keys(keys)); } + /** + * This method submits a search with the _searchString as its query and updates + * the results array accordingly. + */ @action submitSearch = async () => { - this.resetSearch(false); - - //this.props.Document._docFilters = new List(); - this._noResults = ""; + this.resetSearch(); - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.children = 0; - let query = StrCast(this.layoutDoc._searchString); + let query = StrCast(this._searchString); Doc.SetSearchQuery(query); - this._searchFullDB && (query = this.getFinalQuery(query)); this._results = []; - this._resultsSet.clear(); - this._visibleElements = []; - this._visibleDocuments = []; - - if (query || this._searchFullDB === "My Stuff") { - this._endIndex = 12; - this._maxSearchIndex = 0; - this._numTotalResults = -1; - this._searchFullDB ? await this.searchDatabase(query) : this.searchCollection(query); - runInAction(() => { - this.open = this._searchbarOpen = true; - this.resultsScrolled(); - }); - } - } - getAllResults = async (query: string) => { - return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); - } - - private get filterQuery() { - const baseExpr = "NOT system_b:true"; - const authorExpr = this._searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined; - const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true"; - const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; - // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello - return [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, ""); - } - - @computed get primarySort() { - const suffixMap = (type: ColumnType) => { - switch (type) { - case ColumnType.Date: return "_d"; - case ColumnType.String: return "_t"; - case ColumnType.Boolean: return "_b"; - case ColumnType.Number: return "_n"; - } - }; - const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); - return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined); - } - - searchDatabase = async (query: string) => { - this._lockPromise && (await this._lockPromise); - this._lockPromise = new Promise(async res => { - while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: "on", "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { - // happens at the beginning - this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound; - if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { - this._numTotalResults = res.numFound; - } - const highlighting = res.highlighting || {}; - const highlightList = res.docs.map(doc => highlighting[doc[Id]]); - const lines = new Map<string, string[]>(); - res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); - const docs = res.docs; - const highlights: typeof res.highlighting = {}; - docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); - const filteredDocs = this.filterDocsByType(docs); - - runInAction(() => filteredDocs.forEach((doc, i) => { - const index = this._resultsSet.get(doc); - const highlight = highlights[doc[Id]]; - const line = lines.get(doc[Id]) || []; - const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; - // if (this.findCommonElements(hlights)) { - // } - if (index === undefined) { - this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights, line]); - } else { - this._results[index][1].push(...hlights); - this._results[index][2].push(...line); - } - - })); - - this._curRequest = undefined; - })); - this._maxSearchIndex += this._numResultsPerPage; - - await this._curRequest; - } - - this.resultsScrolled(); - - const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0]; - this.docsforfilter = this._results.map(r => r[0]); - this.setSearchFilter(selectedCollection, this.filter ? this.docsforfilter : undefined); - res(); - }); - return this._lockPromise; - } - - startDragCollection = async () => { - const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString))); - const filtered = this.filterDocsByType(res.docs); - const docs = filtered.map(doc => Doc.GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeDelegate(doc) : Doc.MakeAlias(doc)); - let x = 0; - let y = 0; - for (const doc of docs.map(d => Doc.Layout(d))) { - doc.x = x; - doc.y = y; - const size = 200; - const aspect = Doc.NativeHeight(doc) / (Doc.NativeWidth(doc) || 1); - if (aspect > 1) { - doc._height = size; - doc._width = size / aspect; - } else if (aspect > 0) { - doc._width = size; - doc._height = size * aspect; - } else { - doc._width = size; - doc._height = size; - } - x += 250; - if (x > 1000) { - x = 0; - y += 300; - } + if (query) { + this.searchCollection(query); } - const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).map(h => { const v = h[Copy](); v.color = "#f1efeb"; return v; }); - return Docs.Create.SchemaDocument(headers, DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) }); - } - - @action.bound - openSearch(e: React.SyntheticEvent) { - e.stopPropagation(); - this._results.forEach(result => Doc.BrushDoc(result[0])); } - resetSearch = action((close: boolean) => { + /** + * This method resets the search by iterating through each result and removing all + * brushes and highlights. All search matches are cleared as well. + */ + resetSearch = action(() => { this._results.forEach(result => { Doc.UnBrushDoc(result[0]); + Doc.UnHighlightDoc(result[0]); Doc.ClearSearchMatches(); }); - close && (this.open = this._searchbarOpen = false); }); - @action.bound - closeResults() { - this._results = []; - this._resultsSet.clear(); - this._visibleElements = []; - this._visibleDocuments = []; - this._numTotalResults = -1; - this._endIndex = -1; - this._curRequest = undefined; + /** + * @param {Doc} doc - doc to be selected + * + * This method selects a doc by either jumping to it (centering/zooming in on it) + * or opening it in a new tab. + */ + selectElement = async (doc: Doc) => { + await DocumentManager.Instance.jumpToDocument(doc, true); } - @action - resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - this._endIndex = 30; - const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]); - - if (this._numTotalResults <= this._maxSearchIndex) { - this._numTotalResults = this._results.length; - } - - // only hit right at the beginning - // visibleElements is all of the elements (even the ones you can't see) - if (this._visibleElements.length !== this._numTotalResults) { - // undefined until a searchitem is put in there - this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - } - let max = this._numResultsPerPage; - max > this._results.length ? max = this._results.length : console.log(""); - for (let i = this._pageStart; i < max; i++) { - //if the index is out of the window then put a placeholder in - //should ones that have already been found get set to placeholders? - - let result: [Doc, string[], string[]] | undefined = undefined; - - result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - const lines = new List<string>(result[2]); - highlights.forEach((item) => headers.add(item)); - Doc.SetSearchMatch(result[0], { searchMatch: 1 }); - if (i < this._visibleDocuments.length) { - this._visibleDocuments[i] = result[0]; - Doc.BrushDoc(result[0]); - Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); - this.children++; - } - } - } - if (this.props.Document._schemaHeaders === undefined) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); - } - if (this._maxSearchIndex >= this._numTotalResults) { - this._visibleElements.length = this._results.length; - this._visibleDocuments.length = this._results.length; - } + /** + * This method returns a JSX list of the options in the select drop-down menu, which + * is used to filter the types of documents that appear in the search results. + */ + @computed + public get selectOptions() { + const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"] + + return selectValues.map(value => { + return <option key={value} value={value}>{SearchBox.formatType(value)}</option> + }) } - getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight - panelHeight = () => this.props.PanelHeight(); - selectElement = (doc: Doc) => { /* this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); */ }; - returnHeight = () => NumCast(this.layoutDoc._height); - returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length); - - @action - changeSearchScope = (scope: string) => { - this.docsforfilter = undefined; - this.setSearchFilter(this.currentSelectedCollection, undefined); - this._searchFullDB = scope; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.submitSearch(); - } + /** + * This method renders the search input box, select drop-down menu, and search results. + */ + render() { + var validResults = 0; - @computed get scopeButtons() { - return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px" }}> - <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> - <div style={{ display: "contents" }}> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this._searchFullDB} onChange={() => this.changeSearchScope("")} /> - Dashboard - </label> - </div> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={this._searchFullDB?.length ? true : false} onChange={() => this.changeSearchScope("DB")} /> - DB - <span onClick={action(() => this._searchFullDB = this._searchFullDB === "My Stuff" ? "DB" : "My Stuff")}> - {this._searchFullDB === "My Stuff" ? "(me)" : "(full)"} - </span> - </label> - </div> - </div> - </form> - </div>; - } + const results = this._results.map(result => { + var className = "searchBox-results-scroll-view-result"; - setSearchFilter = action((collectionView: { props: { Document: Doc } }, docsForFilter: Doc[] | undefined) => { - if (collectionView) { - const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), null); - collectionView.props.Document._searchFilterDocs = docsForFilter?.length ? new List<Doc>(docsForFilter) : undefined; - collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined; - } - }); + if (this._selectedResult == result[0]) { + className += " searchBox-results-scroll-view-result-selected" + } - render() { - const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); - return ( - <div style={{ pointerEvents: "all" }} className="searchBox-container"> - <div className="searchBox-bar" style={{ background: SearchBox.Instance._undoBackground }}> - <div className="searchBox-lozenges" > - <div className="searchBox-lozenge-user"> - {`${Doc.CurrentUserEmail}`} - <div className="searchBox-logoff" onClick={() => window.location.assign(Utils.prepend("/logout"))}> - Logoff - </div> + if (this._docTypeString == "all" || this._docTypeString == result[0].type) { + validResults++; + return ( + <div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}> + <div className="searchBox-result-title"> + {result[0].title} </div> - <div className="searchBox-lozenge" onClick={() => DocServer.UPDATE_SERVER_CACHE()}> - {`UI project`} + <div className="searchBox-result-type"> + {SearchBox.formatType(StrCast(result[0].type))} </div> - <div className="searchBox-lozenge-dashboard" > - <select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])} - value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}> - {myDashboards.map((dash, i) => <option key={dash[Id]} value={i} style={{ backgroundColor: "black" }}> {StrCast(dash.title)} </option>)} - </select> - <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}> - New - </div> - <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}> - Snapshot - </div> + <div className="searchBox-result-keys"> + {result[1].join(", ")} </div> </div> - <div className="searchBox-query" > - <input defaultValue={""} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef} - className="searchBox-barChild searchBox-input" onKeyPress={this.enter} - style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} /> - <div style={{ display: "flex", alignItems: "center" }}> - <div style={{ position: "absolute", left: 10 }}> - <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}> - <div ref={this.collectionRef}><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" - style={{ cursor: "hand", color: "black", padding: 1, position: "relative" }} /></div> - </Tooltip> - </div> - <div style={{ position: "absolute", left: Doc.UserDoc().noviceMode ? 220 : 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}> - {`${this._results.length}` + " of " + `${this.realTotalResults}`} - </div> - {Doc.UserDoc().noviceMode ? (null) : <div style={{ cursor: "default", left: 235, position: "absolute", }}> - <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} > - <div> - <FontAwesomeIcon icon={"filter"} size="lg" - style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} - onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => this.layoutDoc._searchString ? this.startDragCollection() : undefined); }} - onClick={action(() => this.setSearchFilter(this.currentSelectedCollection, this.filter ? undefined : this.docsforfilter))} /> - </div> - </Tooltip> - </div>} - {this.scopeButtons} - </div> - </div > + ) + } + + return null; + }) + + results.filter(result => result); + + return ( + <div style={{ pointerEvents: "all" }} className="searchBox-container"> + <div className="searchBox-bar"> + <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}> + {this.selectOptions} + </select> + <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" ref={this._inputRef} /> </div > - {!this._searchbarOpen ? (null) : - <div style={{ zIndex: 20000, color: "black" }} ref={(r) => r?.focus()}> - <div style={{ display: "flex", justifyContent: "center", }}> - <div style={{ display: this.open ? "flex" : "none", overflow: "auto", position: "absolute" }}> - <CollectionSchemaView {...this.props} - CollectionView={undefined} - addDocument={returnFalse} - Document={this.props.Document} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelHeight={this.open ? this.returnHeight : returnZero} - PanelWidth={this.open ? this.returnLength : returnZero} - scrollOverflow={length > window.innerWidth || this.children > 6 ? true : false} - focus={this.selectElement} - ScreenToLocalTransform={Transform.Identity} - /> - <div style={{ position: "absolute", right: 5, bottom: 7, width: 15, height: 15, }} - onPointerDown={e => setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - this.props.Document._height = NumCast(this.props.Document._height) + delta[1]; - return false; - }, returnFalse, emptyFunction)} - > - <FontAwesomeIcon icon="grip-lines" size="lg" /> - </div> - </div> - </div> + <div className="searchBox-results-container"> + <div className="searchBox-results-count"> + {`${validResults}` + " result" + (validResults == 1 ? "" : "s")} </div> - } + <div className="searchBox-results-scroll-view"> + {results} + </div> + </div> </div > ); } diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index 164cc29cd..2ecbb536b 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -91,7 +91,7 @@ font-family: 'Roboto'; position: relative; display: flex; - width: 450; + width: fit-content; gap: 5px; .topBar-icon:hover { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 6dcf34a3a..1eeadeedc 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -55,6 +55,9 @@ export namespace Field { || (field instanceof RefField) || (includeUndefined && field === undefined); } + export function Copy(field: any) { + return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; + } } export type Field = number | string | boolean | ObjectField | RefField; export type Opt<T> = T | undefined; @@ -90,7 +93,8 @@ export const DirectLinksSym = Symbol("DirectLinks"); export const AclUnset = Symbol("AclUnset"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); -export const AclAddonly = Symbol("AclAddonly"); +export const AclAugment = Symbol("AclAugment"); +export const AclSelfEdit = Symbol("AclSelfEdit"); export const AclEdit = Symbol("AclEdit"); export const AclAdmin = Symbol("AclAdmin"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); @@ -102,7 +106,8 @@ const AclMap = new Map<string, symbol>([ ["None", AclUnset], [SharingPermissions.None, AclPrivate], [SharingPermissions.View, AclReadonly], - [SharingPermissions.Add, AclAddonly], + [SharingPermissions.Augment, AclAugment], + [SharingPermissions.SelfEdit, AclSelfEdit], [SharingPermissions.Edit, AclEdit], [SharingPermissions.Admin, AclAdmin] ]); @@ -535,7 +540,7 @@ export namespace Doc { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); const copyObjectField = async (field: ObjectField) => { - const list = Cast(doc[key], listSpec(Doc)); + const list = await 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 => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch))); @@ -587,11 +592,10 @@ export namespace Doc { } return copy; } - export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false) { - const cloneMap = new Map<string, Doc>(); + export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<Doc, Doc>(); const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["context", "annotationOn", "cloneOf", "branches", "branchOf"], dontCreate, asBranch); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["cloneOf", "branches", "branchOf"], dontCreate, asBranch); Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { @@ -1214,7 +1218,7 @@ export namespace Doc { case DocumentType.RTF: return "sticky-note"; case DocumentType.COL: const folder: IconProp = isOpen ? "folder-open" : "folder"; - const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right" + const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right"; return !doc?.isFolder ? folder : chevron; case DocumentType.WEB: return "globe-asia"; case DocumentType.SCREENSHOT: return "photo-video"; @@ -1383,7 +1387,7 @@ Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); }); Scripting.addGlobal(function copyDragFactory(dragFactory: Doc) { return Doc.copyDragFactory(dragFactory); }); Scripting.addGlobal(function delegateDragFactory(dragFactory: Doc) { return Doc.delegateDragFactory(dragFactory); }); -Scripting.addGlobal(function copyField(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; }); +Scripting.addGlobal(function copyField(field: any) { return Field.Copy(field); }); Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); Scripting.addGlobal(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); }); Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); }); diff --git a/src/fields/List.ts b/src/fields/List.ts index 215dff34b..93a8d1d60 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -1,4 +1,4 @@ -import { action, observable, runInAction } from "mobx"; +import { action, observable } from "mobx"; import { alias, list, serializable } from "serializr"; import { DocServer } from "../client/DocServer"; import { Scripting } from "../client/util/Scripting"; @@ -264,24 +264,19 @@ class ListImpl<T extends Field> extends ObjectField { // this requests all ProxyFields at the same time to avoid the overhead // of separate network requests and separate updates to the React dom. private __realFields() { - const waiting = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()); - const promised = waiting.map(f => f instanceof ProxyField ? f.promisedValue() : ""); + const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: (f instanceof ProxyField) ? f.promisedValue() : "" })); // if we find any ProxyFields that don't have a current value, then // start the server request for all of them if (promised.length) { - const promise = DocServer.GetRefFields(promised); + const batchPromise = DocServer.GetRefFields(promised.map(p => p.promisedFieldId)); // as soon as we get the fields from the server, set all the list values in one // action to generate one React dom update. - promise.then(fields => runInAction(() => { - waiting.map((w, i) => w instanceof ProxyField && w.setValue(fields[promised[i]])); - })); + batchPromise.then(pfields => promised.forEach(p => p.field.setValue(pfields[p.promisedFieldId]))); // we also have to mark all lists items with this promise so that any calls to them - // will await the batch request. - // This counts on the handler for 'promise' in the call above being invoked before the + // will await the batch request and return the requested field value. + // This assumes the handler for 'promise' in the call above being invoked before the // handler for 'promise' in the lines below. - waiting.map((w, i) => { - w instanceof ProxyField && w.setPromise(promise.then(fields => fields[promised[i]])); - }); + promised.forEach(p => p.field.setPromise(batchPromise.then(pfields => pfields[p.promisedFieldId]))); } return this.__fields.map(toRealField); } diff --git a/src/fields/util.ts b/src/fields/util.ts index 526e5af72..439c4d333 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, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAugment, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing, AclSelfEdit } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -14,6 +14,7 @@ import CursorField from "./CursorField"; import { List } from "./List"; import { SnappingManager } from "../client/util/SnappingManager"; import { computedFn } from "mobx-utils"; +import { RichTextField } from "./RichTextField"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -77,7 +78,9 @@ 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) && !DocServer.Control.isReadOnly();// && !playgroundMode; + const writeToServer = + (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && (value instanceof RichTextField))) && + !DocServer.Control.isReadOnly(); if (writeToDoc) { if (value === undefined) { @@ -138,7 +141,9 @@ export function denormalizeEmail(email: string) { export function inheritParentAcls(parent: Doc, child: Doc) { const dataDoc = parent[DataSym]; for (const key of Object.keys(dataDoc)) { - key.startsWith("acl") && distributeAcls(key, dataDoc[key], child); + // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. + const permission = (key === "acl-Public" && Doc.UserDoc().defaultAclPrivate) ? AclPrivate : dataDoc[key]; + key.startsWith("acl") && distributeAcls(key, permission, child); } } @@ -157,9 +162,10 @@ export function inheritParentAcls(parent: Doc, child: Doc) { */ export enum SharingPermissions { Admin = "Admin", - Edit = "Can Edit", - Add = "Can Augment", - View = "Can View", + Edit = "Edit", + SelfEdit = "Self Edit", + Augment = "Augment", + View = "View", None = "Not Shared" } @@ -176,7 +182,7 @@ export function GetEffectiveAcl(target: any, user?: string): symbol { function getPropAcl(target: any, prop: string | symbol | number) { if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent - if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable + if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable return GetEffectiveAcl(target); } @@ -192,7 +198,8 @@ function getEffectiveAcl(target: any, user?: string): symbol { HierarchyMapping = HierarchyMapping || new Map<symbol, number>([ [AclPrivate, 0], [AclReadonly, 1], - [AclAddonly, 2], + [AclAugment, 2], + [AclSelfEdit, 2.5], [AclEdit, 3], [AclAdmin, 4] ]); @@ -235,6 +242,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc ["Not Shared", 0], ["Can View", 1], ["Can Augment", 2], + ["Self Edit", 2.5], ["Can Edit", 3], ["Admin", 4] ]); @@ -294,7 +302,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; const effectiveAcl = getPropAcl(target, prop); - if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; + if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) 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, "None"].includes(value))) return true; // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true; diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 4ae97913f..224a9eefb 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -283,7 +283,7 @@ export namespace WebSocket { return; } const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((item: any) => item !== undefined) || []; - diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; + diff.diff.$set[updatefield].fields = [...curList, ...newListItems];//, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; Database.Instance.update(diff.id, diff.diff, |