diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/CurrentUserUtils.ts | 62 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 11 | ||||
| -rw-r--r-- | src/client/util/GroupManager.tsx | 139 | ||||
| -rw-r--r-- | src/client/util/GroupMemberView.tsx | 4 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 16 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 8 | ||||
| -rw-r--r-- | src/client/util/SerializationHelper.ts | 2 | ||||
| -rw-r--r-- | src/client/util/SharingManager.tsx | 88 | ||||
| -rw-r--r-- | src/client/util/SnappingManager.ts | 10 |
9 files changed, 159 insertions, 181 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 1096b8e5f..dcbeba8cd 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,6 +1,6 @@ import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; -import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; @@ -8,12 +8,14 @@ import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; +import { SharingPermissions } from "../../fields/util"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; import { DocumentType } from "../documents/DocumentTypes"; +import { Networking } from "../Network"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; @@ -30,9 +32,10 @@ import { Scripting } from "./Scripting"; import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; -import { SharingPermissions } from "../../fields/util"; +import { SnappingManager } from "./SnappingManager"; +export let resolvedPorts: { server: number, socket: number }; const headerViewVersion = "0.1"; export class CurrentUserUtils { private static curr_id: string; @@ -229,7 +232,7 @@ export class CurrentUserUtils { } else { const curButnTypes = Cast(doc["template-buttons"], Doc, null); DocListCastAsync(curButnTypes.data).then(async curBtns => { - await Promise.all(curBtns!); + curBtns && await Promise.all(curBtns); requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype)); }); } @@ -278,7 +281,7 @@ export class CurrentUserUtils { const curNoteTypes = Cast(doc["template-notes"], Doc, null); const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc]; DocListCastAsync(curNoteTypes.data).then(async curNotes => { - await Promise.all(curNotes!); + curNotes && await Promise.all(curNotes); requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype)); }); } @@ -349,7 +352,7 @@ export class CurrentUserUtils { const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc, doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; DocListCastAsync(templateIconsDoc.data).then(async curIcons => { - await Promise.all(curIcons!); + curIcons && await Promise.all(curIcons); requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); }); } @@ -538,9 +541,9 @@ export class CurrentUserUtils { })) as any as Doc; } } - static async setupMenuPanel(doc: Doc, sharingDocumentId: string) { + static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { if (doc.menuStack === undefined) { - await this.setupSharingSidebar(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing + await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) => Docs.Create.FontIconDocument({ icon, @@ -873,7 +876,17 @@ export class CurrentUserUtils { } // Sharing sidebar is where shared documents are contained - static async setupSharingSidebar(doc: Doc, sharingDocumentId: string) { + static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { + if (doc.myLinkDatabase === undefined) { + let linkDocs = await DocServer.GetRefField(linkDatabaseId); + if (!linkDocs) { + linkDocs = new Doc(linkDatabaseId, true); + (linkDocs as Doc).author = Doc.CurrentUserEmail; + (linkDocs as Doc).data = new List<Doc>([]); + (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add; + } + doc.myLinkDatabase = new PrefetchProxy(linkDocs); + } if (doc.mySharedDocs === undefined) { let sharedDocs = await DocServer.GetRefField(sharingDocumentId + "outer"); if (!sharedDocs) { @@ -953,7 +966,15 @@ export class CurrentUserUtils { return doc.clickFuncs as Doc; } - static async updateUserDocument(doc: Doc, sharingDocumentId: string) { + static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { + if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); + const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); + reaction(() => DateCast((doc.globalGroupDatabase as Doc).lastModified), + async () => { + const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); + const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; + SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); + }, { fireImmediately: true }); doc.system = true; doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; @@ -985,10 +1006,8 @@ export class CurrentUserUtils { this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels - await this.setupMenuPanel(doc, sharingDocumentId); - doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); - doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); - if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]); + await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId); + if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered @@ -1009,9 +1028,13 @@ export class CurrentUserUtils { } public static async loadCurrentUser() { - return rp.get(Utils.prepend("/getCurrentUser")).then(response => { + return rp.get(Utils.prepend("/getCurrentUser")).then(async response => { if (response) { - const result: { id: string, email: string } = JSON.parse(response); + const result: { id: string, email: string, cacheDocumentIds: string } = JSON.parse(response); + Doc.CurrentUserEmail = result.email; + resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts")); + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email); + result.cacheDocumentIds && (await DocServer.GetRefFields(result.cacheDocumentIds.split(";"))); return result; } else { throw new Error("There should be a user! Why does Dash think there isn't one?"); @@ -1019,14 +1042,13 @@ export class CurrentUserUtils { }); } - public static async loadUserDocument({ id, email }: { id: string, email: string }) { + public static async loadUserDocument(id: string) { this.curr_id = id; - Doc.CurrentUserEmail = email; await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => { - const { userDocumentId, sharingDocumentId } = JSON.parse(ids); + const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids); if (userDocumentId !== "guest") { return DocServer.GetRefField(userDocumentId).then(async field => - this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId)); + this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId, linkDatabaseId)); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 178daf5f0..a408e1df6 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -164,13 +164,14 @@ export class DocumentManager { const sameContext = annotatedDoc && annotatedDoc === originatingDoc?.context; if (originatingDoc?.isPushpin) { const hide = !docView.props.Document.hidden; - (!hide || !sameContext) && (docView.props.Document.hidden = !docView.props.Document.hidden); - docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target - notfocused && hide && (docView.props.Document.hidden = true); + docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target + if (notfocused || docView.props.Document.hidden) { + docView.props.Document.hidden = !docView.props.Document.hidden; + } return focusAndFinish(); // @ts-ignore bcz: Argh TODO: Need to add a parameter to focus() everywhere for whether focus should center the target's container in the view or not. // here we don't want to focus the container if the source and target are in the same container }, sameContext); - finished?.(); + //finished?.(); } else { docView.select(false); @@ -219,7 +220,7 @@ export class DocumentManager { } } else { // there's no context view so we need to create one first and try again when that finishes createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target - () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished)); + () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished)); } } } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 3264b75f7..a9059ff3d 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -5,15 +5,15 @@ import * as React from "react"; import Select from 'react-select'; import * as RequestPromise from "request-promise"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; -import { StrCast } from "../../fields/Types"; -import { setGroups } from "../../fields/util"; +import { StrCast, Cast } from "../../fields/Types"; import { Utils } from "../../Utils"; -import { DocServer } from "../DocServer"; import { MainViewModal } from "../views/MainViewModal"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; import "./GroupManager.scss"; import { GroupMemberView } from "./GroupMemberView"; import { SharingManager, User } from "./SharingManager"; +import { listSpec } from "../../fields/Schema"; +import { DateField } from "../../fields/DateField"; /** * Interface for options for the react-select component @@ -34,65 +34,23 @@ export class GroupManager extends React.Component<{}> { @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button - private currentUserGroups: string[] = ["Public"]; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private populating: boolean = false; - private groupListener: Opt<Lambda>; - private observableAllGroups: Doc[] = observable([]); - - constructor(props: Readonly<{}>) { super(props); GroupManager.Instance = this; } - /** - * Populates the list of users and groups. - */ - componentDidMount() { - this.populateUsers(); - this.groupListener = autorun(this.populateGroups); - } - - componentWillUnmount() { - this.groupListener?.(); - } + componentDidMount() { this.populateUsers(); } /** * Fetches the list of users stored on the database. */ populateUsers = async () => { - if (!this.populating) { - this.populating = true; - runInAction(() => this.users = []); - const userList = await RequestPromise.get(Utils.prepend("/getUsers")); - const raw = JSON.parse(userList) as User[]; - const evaluating = raw.map(async user => { - const userSharingDocument = await DocServer.GetRefField(user.sharingDocumentId); - if (userSharingDocument instanceof Doc) { - runInAction(() => this.users.push(user.email)); - } - }); - return Promise.all(evaluating).then(() => this.populating = false); - } - } - - /** - * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts - */ - populateGroups = () => { - this.observableAllGroups.map(g => g.members); - DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { - groups?.forEach(group => { - const members: string[] = JSON.parse(StrCast(group.members)); - if (members.includes(Doc.CurrentUserEmail) && this.currentUserGroups.indexOf(StrCast(group.groupName)) === -1) this.currentUserGroups.push(StrCast(group.groupName)); - if (this.observableAllGroups.indexOf(group) === -1) runInAction(() => this.observableAllGroups.push(group)); - }); - setGroups(this.currentUserGroups); - console.log("populating groups"); - }); + const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const raw = JSON.parse(userList) as User[]; + raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); } /** @@ -110,7 +68,6 @@ export class GroupManager extends React.Component<{}> { // SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); - this.populateGroups(); } /** @@ -129,25 +86,24 @@ export class GroupManager extends React.Component<{}> { /** * @returns the database of groups. */ - get GroupManagerDoc(): Doc | undefined { - return Doc.UserDoc().globalGroupDatabase as Doc; - } + @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; } /** * @returns a list of all group documents. */ - getAllGroups(): Doc[] { - const groupDoc = this.GroupManagerDoc; - return groupDoc ? DocListCast(groupDoc.data) : []; - } + @computed get allGroups(): Doc[] { return DocListCast(this.GroupManagerDoc?.data); } + + /** + * @returns the members of the admin group. + */ + @computed get adminGroupMembers(): string[] { return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; } /** * @returns a group document based on the group name. * @param groupName */ getGroup(groupName: string): Doc | undefined { - const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); - return groupDoc; + return this.allGroups.find(group => group.title === groupName); } /** @@ -155,15 +111,9 @@ export class GroupManager extends React.Component<{}> { */ getGroupMembers(group: string | Doc): string[] { if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[]; - else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; + return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; } - /** - * @returns the members of the admin group. - */ - get adminGroupMembers(): string[] { - return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; - } /** * @returns a boolean indicating whether the current user has access to edit group documents. @@ -181,14 +131,11 @@ export class GroupManager extends React.Component<{}> { * @param memberEmails */ createGroupDoc(groupName: string, memberEmails: string[] = []) { - const groupDoc = new Doc; - groupDoc.groupName = groupName.toLowerCase() === "admin" ? "Admin" : groupName; + const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName; + const groupDoc = new Doc("GROUP:" + name, true); + groupDoc.title = name; groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); - if (memberEmails.includes(Doc.CurrentUserEmail)) { - this.currentUserGroups.push(groupName); - setGroups(this.currentUserGroups); - } this.addGroup(groupDoc); } @@ -199,6 +146,7 @@ export class GroupManager extends React.Component<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc); + this.GroupManagerDoc.lastModified = new DateField; return true; } return false; @@ -208,19 +156,20 @@ export class GroupManager extends React.Component<{}> { * Deletes a group from the database of group documents and @returns whether the group was deleted or not. * @param group */ + @action deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); SharingManager.Instance.removeGroup(group); - const members: string[] = JSON.parse(StrCast(group.members)); + const members = JSON.parse(StrCast(group.members)); if (members.includes(Doc.CurrentUserEmail)) { - const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName); - index !== -1 && this.currentUserGroups.splice(index, 1); - setGroups(this.currentUserGroups); + const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); + index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } + this.GroupManagerDoc.lastModified = new DateField; if (group === this.currentGroup) { - runInAction(() => this.currentGroup = undefined); + this.currentGroup = undefined; } return true; } @@ -235,10 +184,11 @@ export class GroupManager extends React.Component<{}> { */ addMemberToGroup(groupDoc: Doc, email: string) { if (this.hasEditAccess(groupDoc)) { - const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); + const memberList = JSON.parse(StrCast(groupDoc.members)); !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); } } @@ -249,12 +199,13 @@ export class GroupManager extends React.Component<{}> { */ removeMemberFromGroup(groupDoc: Doc, email: string) { if (this.hasEditAccess(groupDoc)) { - const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); + const memberList = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); if (index !== -1) { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); } } } @@ -384,14 +335,13 @@ export class GroupManager extends React.Component<{}> { private get groupInterface() { const sortGroups = (d1: Doc, d2: Doc) => { - const g1 = StrCast(d1.groupName); - const g2 = StrCast(d2.groupName); + const g1 = StrCast(d1.title); + const g2 = StrCast(d2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; }; - let groups = this.getAllGroups(); - groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups; + const groups = this.groupSort === "ascending" ? this.allGroups.sort(sortGroups) : this.groupSort === "descending" ? this.allGroups.sort(sortGroups).reverse() : this.allGroups; return ( <div className="group-interface"> @@ -424,9 +374,9 @@ export class GroupManager extends React.Component<{}> { {groups.map(group => <div className="group-row" - key={StrCast(group.groupName)} + key={StrCast(group.title || group.groupName)} > - <div className="group-name" >{group.groupName}</div> + <div className="group-name" >{group.title || group.groupName}</div> <div className="group-info" onClick={action(() => this.currentGroup = group)}> <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> </div> @@ -440,16 +390,13 @@ export class GroupManager extends React.Component<{}> { } render() { - return ( - <MainViewModal - contents={this.groupInterface} - isDisplayed={this.isOpen} - interactive={true} - dialogueBoxStyle={{ zIndex: 1002 }} - overlayStyle={{ zIndex: 1001 }} - closeOnExternalClick={this.close} - /> - ); + return <MainViewModal + contents={this.groupInterface} + isDisplayed={this.isOpen} + interactive={true} + dialogueBoxStyle={{ zIndex: 1002 }} + overlayStyle={{ zIndex: 1001 }} + closeOnExternalClick={this.close} + />; } - }
\ No newline at end of file diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index 4ead01e9f..927200ed3 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -33,8 +33,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> { <input className="group-title" style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }} - value={StrCast(this.props.group.groupName)} - onChange={e => this.props.group.groupName = e.currentTarget.value} + value={StrCast(this.props.group.title || this.props.group.groupName)} + onChange={e => this.props.group.title = e.currentTarget.value} disabled={!hasEditAccess} > </input> diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 56b6cb8a9..1ba6cff6d 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,6 +1,4 @@ import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { listSpec } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; import { SharingManager } from "./SharingManager"; import { computedFn } from "mobx-utils"; @@ -36,17 +34,21 @@ export class LinkManager { public getAllLinks(): Doc[] { - const lset = new Set<Doc>(DocListCast(Doc.UserDoc().myLinkDatabase)); - SharingManager.Instance.users.forEach(user => DocListCast(user.sharingDoc.myLinkDatabase).map(lset.add)); + const lset = new Set<Doc>(DocListCast(Doc.LinkDBDoc().data)); + SharingManager.Instance.users.forEach(user => { + DocListCast(user.linkDatabase?.data).map(doc => { + lset.add(doc); + }); + }); return Array.from(lset); } public addLink(linkDoc: Doc): boolean { - return Doc.AddDocToList(Doc.UserDoc(), "myLinkDatabase", linkDoc); + return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); } public deleteLink(linkDoc: Doc): boolean { - return Doc.RemoveDocFromList(Doc.UserDoc(), "myLinkDatabase", linkDoc); + return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } // finds all links that contain the given anchor @@ -67,7 +69,7 @@ export class LinkManager { related.push(...LinkManager.Instance.getAllRelatedLinks(anno)); }); return related; - }.bind(this)); + }.bind(this), true); // finds all links that contain the given anchor public getAllRelatedLinks(anchor: Doc): Doc[] { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 008ce281c..34e88c7b0 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,7 +2,6 @@ import { observable, action, runInAction, ObservableMap } from "mobx"; import { Doc, Opt } from "../../fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; -import { List } from "../../fields/List"; import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; import { CollectionViewType } from "../views/collections/CollectionView"; @@ -67,15 +66,16 @@ export namespace SelectionManager { manager.SelectSchemaDoc(colSchema, document); } + const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed + return manager.SelectedDocuments.get(doc) ? true : false; + }); // computed functions, such as used in IsSelected generate errors if they're called outside of a // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature // to avoid unnecessary mobx invalidations when running inside a reaction. export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean { return !doc ? false : outsideReaction ? manager.SelectedDocuments.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() - computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed - return manager.SelectedDocuments.get(doc) ? true : false; - })(doc); + IsSelectedCache(doc); } export function DeselectAll(except?: Doc): void { diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 19b217726..00ac6e521 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -43,7 +43,7 @@ export namespace SerializationHelper { } if (!obj.__type) { - if (ClientUtils.RELEASE) { + if (true || ClientUtils.RELEASE) { console.warn("No property 'type' found in JSON."); return undefined; } else { diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 742811c18..360fad998 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -26,6 +26,7 @@ import { SearchBox } from "../views/search/SearchBox"; export interface User { email: string; sharingDocumentId: string; + linkDatabaseId: string; } /** @@ -52,6 +53,7 @@ const storage = "data"; interface ValidatedUser { user: User; // database minimal info to identify / communicate with a user (email, sharing doc id) sharingDoc: Doc; // document to share/message another user + linkDatabase: Doc; userColor: string; // stored on the sharinDoc, extracted for convenience? } @@ -138,8 +140,10 @@ export class SharingManager extends React.Component<{}> { const isCandidate = user.email !== Doc.CurrentUserEmail; if (isCandidate) { const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId); - if (sharingDoc instanceof Doc) { - sharingDocs.push({ user, sharingDoc, userColor: StrCast(sharingDoc.color) }); + const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId); + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + await DocListCastAsync(linkDatabase.data); + sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.color) }); } } }); @@ -161,10 +165,10 @@ export class SharingManager extends React.Component<{}> { * @param group * @param permission */ - setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => { + setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { const target = targetDoc || this.targetDoc!; - const key = normalizeEmail(StrCast(group.groupName)); + const key = normalizeEmail(StrCast(group.title)); const acl = `acl-${key}`; const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); @@ -181,8 +185,8 @@ export class SharingManager extends React.Component<{}> { group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]); users.forEach(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(sharingDoc[storage])) === -1 && Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added - else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists + if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists }); } }); @@ -193,22 +197,18 @@ export class SharingManager extends React.Component<{}> { * @param group * @param emailId */ - shareWithAddedMember = (group: Doc, emailId: string) => { - const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; + shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { + const user = this.users.find(({ user: { email } }) => email === emailId)!; + const self = this; if (group.docsShared) { - DocListCastAsync(group.docsShared).then(async docs => { - if (docs) { - const memberDocs = await DocListCastAsync(user.sharingDoc[storage]); - memberDocs && docs.forEach(doc => { - const index = Doc.IndexOf(doc, memberDocs); - console.log(index); - console.log(doc); - index === -1 && (console.log(Doc.AddDocToList(user.sharingDoc, storage, doc))); - }); - } - - }); - // === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc)); + if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); + else { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + })); + } } } @@ -239,16 +239,12 @@ export class SharingManager extends React.Component<{}> { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { - DocListCastAsync(group.docsShared).then(docs => { - docs?.forEach(doc => { - DocListCastAsync(user.sharingDoc[storage]).then(sharedDocs => { - sharedDocs && Doc.IndexOf(doc, sharedDocs) !== -1 && Doc.RemoveDocFromList(user.sharingDoc, storage, doc); - }); - }); - }); - // DocListCast(group.docsShared).forEach(doc => { - // Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.sharingDoc, storage, doc); // remove the doc only if it is in the list - // }); + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); } } @@ -259,7 +255,7 @@ export class SharingManager extends React.Component<{}> { removeGroup = (group: Doc) => { if (group.docsShared) { DocListCast(group.docsShared).forEach(doc => { - const acl = `acl-${StrCast(group.groupName)}`; + const acl = `acl-${StrCast(group.title)}`; distributeAcls(acl, SharingPermissions.None, doc); @@ -285,8 +281,8 @@ export class SharingManager extends React.Component<{}> { doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); distributeAcls(acl, permission as SharingPermissions, doc); - if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(sharingDoc[storage])) === -1 && Doc.AddDocToList(sharingDoc, storage, doc); - else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); + if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); }); } @@ -429,8 +425,8 @@ export class SharingManager extends React.Component<{}> { * Sorting algorithm to sort groups. */ sortGroups = (group1: Doc, group2: Doc) => { - const g1 = StrCast(group1.groupName); - const g2 = StrCast(group2.groupName); + const g1 = StrCast(group1.title); + const g2 = StrCast(group2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; } @@ -439,9 +435,9 @@ export class SharingManager extends React.Component<{}> { */ @computed get sharingInterface() { TraceMobx(); - const groupList = GroupManager.Instance?.getAllGroups() || []; + const groupList = GroupManager.Instance?.allGroups || []; const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); + const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) })); // the next block handles the users shown (individuals/groups/both) const options: GroupedOptions[] = []; @@ -479,7 +475,7 @@ export class SharingManager extends React.Component<{}> { const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))); // the list of users shared with - const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, sharingDoc, userColor }) => { + const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => { const userKey = `acl-${normalizeEmail(user.email)}`; const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]); const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-"; @@ -495,7 +491,7 @@ export class SharingManager extends React.Component<{}> { <select className={"permissions-dropdown"} value={permissions} - onChange={e => this.setInternalSharing({ user, sharingDoc: sharingDoc, userColor }, e.currentTarget.value)} + onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)} > {this.sharingOptions(uniform)} </select> @@ -546,19 +542,19 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with - const groupListMap: (Doc | { groupName: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : true); - groupListMap.unshift({ groupName: "Public" }, { groupName: "Override" }); + const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true); + groupListMap.unshift({ title: "Public" }, { title: "Override" }); const groupListContents = groupListMap.map(group => { - const groupKey = `acl-${StrCast(group.groupName)}`; + const groupKey = `acl-${StrCast(group.title)}`; const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]); - const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.groupName)}`]) : "-multiple-"; + const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-"; return !permissions ? (null) : ( <div key={groupKey} className={"container"} > - <div className={"padding"}>{group.groupName}</div> + <div className={"padding"}>{group.title}</div> {group instanceof Doc ? (<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}> <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> @@ -571,7 +567,7 @@ export class SharingManager extends React.Component<{}> { value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} > - {this.sharingOptions(uniform, group.groupName === "Override")} + {this.sharingOptions(uniform, group.title === "Override")} </select> ) : ( <div className={"permissions-dropdown"}> diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index fc07e8ab4..069f81d38 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,4 +1,5 @@ import { observable, action, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; export namespace SnappingManager { @@ -14,6 +15,9 @@ export namespace SnappingManager { this.horizSnapLines = horizLines; this.vertSnapLines = vertLines; } + + @observable cachedGroups: string[] = []; + @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; } } const manager = new Manager(); @@ -25,5 +29,11 @@ export namespace SnappingManager { export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } export function GetIsDragging() { return manager.IsDragging; } + + /// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts + // need to investigate further what caused the mobx update problems and move to a better location. + const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true); + export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); } + export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); } } |
