diff options
Diffstat (limited to 'src')
42 files changed, 485 insertions, 309 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 142c37ea4..48886aa3b 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"; @@ -577,7 +577,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; @@ -588,13 +588,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; } @@ -699,7 +701,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; @@ -721,15 +723,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); } @@ -1033,8 +1040,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 9a634017a..e446e3752 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -460,7 +460,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); @@ -773,8 +773,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; } @@ -923,9 +928,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); } @@ -934,13 +940,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); } @@ -1047,7 +1058,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); @@ -1246,15 +1257,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); @@ -1324,17 +1326,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/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 a07f4037a..6a388c5b4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -104,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))); 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/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/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/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, |