From c63d7ab902135165eaafc89bcea15805ff10f11b Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 8 Oct 2020 19:54:52 -0400 Subject: load entire DB in one message. --- src/client/util/CurrentUserUtils.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 1096b8e5f..d011d7b09 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -31,8 +31,10 @@ import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; import { SharingPermissions } from "../../fields/util"; +import { Networking } from "../Network"; +export let resolvedPorts: { server: number, socket: number }; const headerViewVersion = "0.1"; export class CurrentUserUtils { private static curr_id: string; @@ -1009,9 +1011,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,9 +1025,8 @@ 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); if (userDocumentId !== "guest") { -- cgit v1.2.3-70-g09d2 From 565ab27220e2e626b88642929a4091d3ced893e7 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 01:14:19 -0400 Subject: fixed sharing to share with new user added during session. --- src/client/util/SharingManager.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index de9402ff0..1cc4c59f2 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -184,9 +184,13 @@ 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)!; - if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc)); + shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { + const user = this.users.find(({ user: { email } }) => email === emailId)!; + const self = this; + if (group.docsShared) { + if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); + else DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc)); + } } /** -- cgit v1.2.3-70-g09d2 From d4e967ed8fc2f99d44b887ebabf0a4eb642ecaab Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 01:47:59 -0400 Subject: simplified setting group users --- src/client/util/GroupManager.tsx | 10 +--------- src/fields/Doc.ts | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index fb3342e68..48e3ca737 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -59,17 +59,9 @@ export class GroupManager extends React.Component<{}> { */ 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); + raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); } } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 54d85ba86..d85f0785e 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -25,7 +25,6 @@ import JSZip = require("jszip"); import { saveAs } from "file-saver"; import { CollectionDockingView } from "../client/views/collections/CollectionDockingView"; import { SelectionManager } from "../client/util/SelectionManager"; -import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { -- cgit v1.2.3-70-g09d2 From c8e8cbe52a97835342c3a3d04033462f4497cd72 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 11:36:56 -0400 Subject: trying to get group updates to work automatically --- src/client/documents/Documents.ts | 2 ++ src/client/util/CurrentUserUtils.ts | 7 ++-- src/client/util/GroupManager.tsx | 71 ++++++++++++------------------------- src/client/util/GroupMemberView.tsx | 4 +-- src/client/util/SharingManager.tsx | 24 ++++++------- src/fields/util.ts | 39 ++++++++++++-------- 6 files changed, 67 insertions(+), 80 deletions(-) (limited to 'src/client/util') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7ee8267f8..7d78bd76a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -244,6 +244,7 @@ export namespace Docs { view: LayoutSource, dataField: string }, + data?: any, options?: Partial }; type TemplateMap = Map; @@ -464,6 +465,7 @@ export namespace Docs { const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) }; options.layout = layout.view?.LayoutString(layout.dataField); const doc = Doc.assign(new Doc(prototypeId, true), { system: true, layoutKey: "layout", ...options }); + doc.data = template.data; doc.layout_keyValue = KeyValueBox.LayoutString(""); return doc; } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index d011d7b09..b4e24f70e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -30,7 +30,7 @@ import { Scripting } from "./Scripting"; import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; -import { SharingPermissions } from "../../fields/util"; +import { SharingPermissions, UserGroups } from "../../fields/util"; import { Networking } from "../Network"; @@ -988,9 +988,10 @@ export class CurrentUserUtils { 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.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); + if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]); + UserGroups.setCurrentUserGroups((doc.globalGroupDatabase as Doc).data as List); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 48e3ca737..a0db46fdb 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,19 +1,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; 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 { Doc, DocListCast, Opt } from "../../fields/Doc"; import { Cast, StrCast } from "../../fields/Types"; -import { setGroups } from "../../fields/util"; 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"; /** * Interface for options for the react-select component @@ -34,12 +33,8 @@ export class GroupManager extends React.Component<{}> { @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject = React.createRef(); // the ref for the input box. private createGroupButtonRef: React.RefObject = React.createRef(); // the ref for the group creation button - private currentUserGroups: string[] = []; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private populating: boolean = false; - - constructor(props: Readonly<{}>) { super(props); @@ -51,32 +46,15 @@ export class GroupManager extends React.Component<{}> { */ componentDidMount() { this.populateUsers(); - this.populateGroups(); } /** * Fetches the list of users stored on the database. */ populateUsers = async () => { - if (!this.populating) { - 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))); - } - } - - /** - * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts - */ - populateGroups = () => { - DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { - groups?.forEach(group => { - const members: string[] = JSON.parse(StrCast(group.members)); - if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); - }); - this.currentUserGroups.push("Public"); - setGroups(this.currentUserGroups); - }); + 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))); } /** @@ -94,7 +72,6 @@ export class GroupManager extends React.Component<{}> { // SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); - this.populateGroups(); } /** @@ -113,14 +90,14 @@ export class GroupManager extends React.Component<{}> { /** * @returns the database of groups. */ - get GroupManagerDoc(): Doc | undefined { + @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; } /** * @returns a list of all group documents. */ - getAllGroups(): Doc[] { + @computed get allGroups(): Doc[] { const groupDoc = this.GroupManagerDoc; return groupDoc ? DocListCast(groupDoc.data) : []; } @@ -130,7 +107,7 @@ export class GroupManager extends React.Component<{}> { * @param groupName */ getGroup(groupName: string): Doc | undefined { - const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); + const groupDoc = this.allGroups.find(group => group.title === groupName); return groupDoc; } @@ -164,15 +141,13 @@ export class GroupManager extends React.Component<{}> { * @param groupName * @param memberEmails */ + @action 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); + 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); } @@ -192,19 +167,19 @@ 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); } if (group === this.currentGroup) { - runInAction(() => this.currentGroup = undefined); + this.currentGroup = undefined; } return true; } @@ -368,13 +343,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(); + let groups = this.allGroups; groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups; return ( @@ -408,9 +383,9 @@ export class GroupManager extends React.Component<{}> { {groups.map(group =>
-
{group.groupName}
+
{group.title}
this.currentGroup = group)}>
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index 4ead01e9f..d9174561c 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -33,8 +33,8 @@ export class GroupMemberView extends React.Component { this.props.group.groupName = e.currentTarget.value} + value={StrCast(this.props.group.title)} + onChange={e => this.props.group.title = e.currentTarget.value} disabled={!hasEditAccess} > diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 1cc4c59f2..a9abeedaf 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -152,10 +152,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); @@ -233,7 +233,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); @@ -411,8 +411,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; } @@ -421,7 +421,7 @@ 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) })); @@ -527,19 +527,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(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : 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) : (
-
{group.groupName}
+
{group.title}
{group instanceof Doc ? (
GroupManager.Instance.currentGroup = group)}> @@ -552,7 +552,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")} ) : (
diff --git a/src/fields/util.ts b/src/fields/util.ts index 7293db0c2..1e6707a9a 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -4,7 +4,7 @@ import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; -import { action, trace } from "mobx"; +import { action, trace, observable, reaction } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; @@ -22,6 +22,24 @@ export function TraceMobx() { tracing && trace(); } +// the list of groups that the current user is a member of +export class UserGroups { + @observable static Current: string[]; + @action static setCurrentUserGroups(groupList: List) { + reaction(() => groupList, + groupList => { + UserGroups.Current = [ + "Public", + ...(groupList as List). + filter(group => group instanceof Doc). + map(group => group as Doc). + filter(group => JSON.parse(StrCast(group.members))?.includes(Doc.CurrentUserEmail)). + map(group => StrCast(group.title)) + ]; + }, { fireImmediately: true }); + } +} + export interface GetterResult { value: FieldResult; shouldReturn?: boolean; @@ -130,14 +148,6 @@ export function denormalizeEmail(email: string) { // playgroundMode = !playgroundMode; // } -// the list of groups that the current user is a member of -let currentUserGroups: string[] = []; - -// called from GroupManager once the groups have been fetched from the server -export function setGroups(groups: string[]) { - currentUserGroups = groups; -} - /** * These are the various levels of access a user can have to a document. * @@ -164,6 +174,7 @@ export enum SharingPermissions { */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { if (!target) return AclPrivate; + if (target[Id] === "groupdbProto" || target[Id]?.startsWith("GROUP:")) return AclAdmin; // all changes received fromt the server must be processed as Admin if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; @@ -171,7 +182,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, // if the current user is the author of the document / the current user is a member of the admin group const userChecked = user || Doc.CurrentUserEmail; if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; - if (currentUserGroups.includes("Admin")) return AclAdmin; + if (UserGroups.Current?.includes("Admin")) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { @@ -192,7 +203,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group - if (currentUserGroups.includes(entity) || userChecked === entity) { + if (UserGroups.Current?.includes(entity) || userChecked === entity) { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; if (effectiveAcl === AclAdmin) return effectiveAcl; @@ -308,12 +319,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === "toString" || in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; + if (in_prop === "toString" || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === "symbol")) return target.__fields[prop] || target[prop]; if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; - if (prop === LayoutSym) { - return target.__LAYOUT__; - } + if (prop === LayoutSym) return target.__LAYOUT__; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); -- cgit v1.2.3-70-g09d2 From ec8673566721a4625466401502c2e16878658007 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 13:02:35 -0400 Subject: less broken, but still not working --- src/client/util/CurrentUserUtils.ts | 13 +++++++------ src/client/util/GroupManager.tsx | 12 +++++------- src/client/util/SharingManager.tsx | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b4e24f70e..759fd6eca 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -10,10 +10,12 @@ import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; +import { SharingPermissions, UserGroups } 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,8 +32,6 @@ import { Scripting } from "./Scripting"; import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; -import { SharingPermissions, UserGroups } from "../../fields/util"; -import { Networking } from "../Network"; export let resolvedPorts: { server: number, socket: number }; @@ -231,7 +231,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)); }); } @@ -280,7 +280,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)); }); } @@ -351,7 +351,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)); }); } @@ -989,8 +989,9 @@ export class CurrentUserUtils { await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels await this.setupMenuPanel(doc, sharingDocumentId); if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); - if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); + doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]); + const x = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); UserGroups.setCurrentUserGroups((doc.globalGroupDatabase as Doc).data as List); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index a0db46fdb..f579f3dae 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,11 +1,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import Select from 'react-select'; import * as RequestPromise from "request-promise"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { Cast, StrCast } from "../../fields/Types"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; +import { StrCast, Cast } from "../../fields/Types"; import { Utils } from "../../Utils"; import { MainViewModal } from "../views/MainViewModal"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; @@ -98,8 +98,7 @@ export class GroupManager extends React.Component<{}> { * @returns a list of all group documents. */ @computed get allGroups(): Doc[] { - const groupDoc = this.GroupManagerDoc; - return groupDoc ? DocListCast(groupDoc.data) : []; + return DocListCast(this.GroupManagerDoc?.data); } /** @@ -141,10 +140,9 @@ export class GroupManager extends React.Component<{}> { * @param groupName * @param memberEmails */ - @action createGroupDoc(groupName: string, memberEmails: string[] = []) { const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName; - const groupDoc = new Doc("GROUP:" + name); + const groupDoc = new Doc("GROUP:" + name, true); groupDoc.title = name; groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index a9abeedaf..729742890 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -423,7 +423,7 @@ export class SharingManager extends React.Component<{}> { TraceMobx(); 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[] = []; @@ -527,7 +527,7 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with - const groupListMap: (Doc | { title: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : true); + 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.title)}`; -- cgit v1.2.3-70-g09d2 From 2608cc4dbf852a318b9d6a453bdaf64249917cda Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 16:54:21 -0400 Subject: fixed updating groups to take effect immediately --- src/client/util/CurrentUserUtils.ts | 6 +++--- src/client/util/SharingManager.tsx | 12 +++++++----- src/fields/util.ts | 26 +++++++++++++++++--------- 3 files changed, 27 insertions(+), 17 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 759fd6eca..580c6040e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -956,6 +956,9 @@ export class CurrentUserUtils { } static async updateUserDocument(doc: Doc, sharingDocumentId: string) { + if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); + await DocListCastAsync((doc.globalGroupDatabase as Doc).data); + UserGroups.Current; doc.system = true; doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; @@ -989,10 +992,7 @@ export class CurrentUserUtils { await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels await this.setupMenuPanel(doc, sharingDocumentId); if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); - doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]); - const x = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); - UserGroups.setCurrentUserGroups((doc.globalGroupDatabase as Doc).data as List); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 729742890..ee397ab4f 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import Select from "react-select"; import * as RequestPromise from "request-promise"; -import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly } from "../../fields/Doc"; +import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly, DocListCastAsync } from "../../fields/Doc"; import { List } from "../../fields/List"; import { Cast, StrCast } from "../../fields/Types"; import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail } from "../../fields/util"; @@ -189,7 +189,9 @@ export class SharingManager extends React.Component<{}> { const self = this; if (group.docsShared) { if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); - else DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc)); + else DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => { + Doc.AddDocToList(user.sharingDoc, storage, doc); + })); } } @@ -220,9 +222,9 @@ export class SharingManager extends React.Component<{}> { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { - 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(group.docsShared).then(dl => dl?.forEach(doc => { + Doc.RemoveDocFromList(user.sharingDoc, storage, doc); + })); } } diff --git a/src/fields/util.ts b/src/fields/util.ts index 1e6707a9a..791b98b83 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -4,7 +4,7 @@ import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; -import { action, trace, observable, reaction } from "mobx"; +import { action, trace, observable, reaction, computed } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; @@ -24,19 +24,28 @@ export function TraceMobx() { // the list of groups that the current user is a member of export class UserGroups { - @observable static Current: string[]; - @action static setCurrentUserGroups(groupList: List) { - reaction(() => groupList, - groupList => { - UserGroups.Current = [ + static computing = false; + static cachedGroups: string[] = []; + static globalGroupDoc: Doc | undefined; + static get Current() { + if (!Doc.UserDoc() || UserGroups.computing) return UserGroups.cachedGroups; + UserGroups.computing = true; + if (!UserGroups.globalGroupDoc) UserGroups.globalGroupDoc = Doc.UserDoc().globalGroupDatabase as Doc; + if (UserGroups.globalGroupDoc) { + const dbgroups = DocListCast(UserGroups.globalGroupDoc.data); + if (dbgroups.length !== UserGroups.cachedGroups.length - 1) { + UserGroups.cachedGroups = [ "Public", - ...(groupList as List). + ...dbgroups?. filter(group => group instanceof Doc). map(group => group as Doc). filter(group => JSON.parse(StrCast(group.members))?.includes(Doc.CurrentUserEmail)). map(group => StrCast(group.title)) ]; - }, { fireImmediately: true }); + } + } + UserGroups.computing = false; + return UserGroups.cachedGroups; } } @@ -174,7 +183,6 @@ export enum SharingPermissions { */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { if (!target) return AclPrivate; - if (target[Id] === "groupdbProto" || target[Id]?.startsWith("GROUP:")) return AclAdmin; // all changes received fromt the server must be processed as Admin if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; -- cgit v1.2.3-70-g09d2 From 2555972e14046d7042ed575db1d80c8a7ba06ae6 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 17:07:06 -0400 Subject: from last --- src/client/util/GroupManager.tsx | 57 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 36 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index f579f3dae..4bb5a93fc 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -41,12 +41,7 @@ export class GroupManager extends React.Component<{}> { GroupManager.Instance = this; } - /** - * Populates the list of users and groups. - */ - componentDidMount() { - this.populateUsers(); - } + componentDidMount() { this.populateUsers(); } /** * Fetches the list of users stored on the database. @@ -90,24 +85,24 @@ export class GroupManager extends React.Component<{}> { /** * @returns the database of groups. */ - @computed 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. */ - @computed get allGroups(): Doc[] { - return DocListCast(this.GroupManagerDoc?.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.allGroups.find(group => group.title === groupName); - return groupDoc; + return this.allGroups.find(group => group.title === groupName); } /** @@ -115,15 +110,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. @@ -192,7 +181,7 @@ 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); @@ -206,7 +195,7 @@ 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]; @@ -347,8 +336,7 @@ export class GroupManager extends React.Component<{}> { return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; }; - let groups = this.allGroups; - 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 (
@@ -397,16 +385,13 @@ export class GroupManager extends React.Component<{}> { } render() { - return ( - - ); + return ; } - } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From a97f2f001af346629247a55d5efd19eaa406bb10 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 9 Oct 2020 17:16:09 -0400 Subject: backward compatibiilty with groupName --- src/client/util/GroupManager.tsx | 4 ++-- src/client/util/GroupMemberView.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 4bb5a93fc..63e2a8024 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -369,9 +369,9 @@ export class GroupManager extends React.Component<{}> { {groups.map(group =>
-
{group.title}
+
{group.title || group.groupName}
this.currentGroup = group)}>
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index d9174561c..927200ed3 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -33,7 +33,7 @@ export class GroupMemberView extends React.Component { this.props.group.title = e.currentTarget.value} disabled={!hasEditAccess} > -- cgit v1.2.3-70-g09d2 From f3d04b73a53d7680092ce925fceede4f910df5bb Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 10 Oct 2020 12:10:48 -0400 Subject: fixes to issues with updating interactively whe groups change. fixes for multiple rapid modifications to list field where values would be overwrriten when sever sendBack stale information. fixes to list.splice() where deleting nothing but adding something didn't result in an addToSet. --- src/client/util/CurrentUserUtils.ts | 14 +++++-- src/client/util/GroupManager.tsx | 5 +++ src/client/util/SharingManager.tsx | 28 ++++++++----- src/client/util/SnappingManager.ts | 8 ++++ .../views/collections/CollectionStackingView.tsx | 1 - src/fields/List.ts | 5 ++- src/fields/util.ts | 46 ++++++---------------- src/server/websocket.ts | 6 ++- 8 files changed, 61 insertions(+), 52 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 580c6040e..694982fea 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -8,9 +8,9 @@ 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, UserGroups } from "../../fields/util"; +import { SharingPermissions } from "../../fields/util"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; @@ -32,6 +32,7 @@ import { Scripting } from "./Scripting"; import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; +import { SnappingManager } from "./SnappingManager"; export let resolvedPorts: { server: number, socket: number }; @@ -957,8 +958,13 @@ export class CurrentUserUtils { static async updateUserDocument(doc: Doc, sharingDocumentId: string) { if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); - await DocListCastAsync((doc.globalGroupDatabase as Doc).data); - UserGroups.Current; + 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; diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 63e2a8024..cc1d45a58 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -13,6 +13,7 @@ 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 @@ -144,6 +145,7 @@ export class GroupManager extends React.Component<{}> { */ addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { + this.GroupManagerDoc.lastModified = new DateField; Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc); return true; } @@ -158,6 +160,7 @@ export class GroupManager extends React.Component<{}> { deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { + this.GroupManagerDoc.lastModified = new DateField; Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); SharingManager.Instance.removeGroup(group); const members = JSON.parse(StrCast(group.members)); @@ -184,6 +187,7 @@ export class GroupManager extends React.Component<{}> { const memberList = JSON.parse(StrCast(groupDoc.members)); !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); SharingManager.Instance.shareWithAddedMember(groupDoc, email); } } @@ -200,6 +204,7 @@ export class GroupManager extends React.Component<{}> { if (index !== -1) { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); SharingManager.Instance.removeMember(groupDoc, email); } } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index ee397ab4f..16bcd46c8 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -22,6 +22,7 @@ import "./SharingManager.scss"; import { SelectionManager } from "./SelectionManager"; import { intersection } from "lodash"; import { SearchBox } from "../views/search/SearchBox"; +import { listSpec } from "../../fields/Schema"; export interface User { email: string; @@ -172,8 +173,8 @@ export class SharingManager extends React.Component<{}> { group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List).push(doc) : group.docsShared = new List([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, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists }); } }); @@ -189,9 +190,13 @@ export class SharingManager extends React.Component<{}> { const self = this; if (group.docsShared) { if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); - else DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => { - Doc.AddDocToList(user.sharingDoc, storage, doc); - })); + else { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + })); + } } } @@ -222,9 +227,12 @@ export class SharingManager extends React.Component<{}> { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { - DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => { - Doc.RemoveDocFromList(user.sharingDoc, storage, doc); - })); + 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); + }) + ); } } @@ -261,8 +269,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, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); }); } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index fc07e8ab4..d067dff6c 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -14,6 +14,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 +28,10 @@ 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. + export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); } + export function GetCachedGroups() { return manager.cachedGroups; } } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index daaee67dc..72ce041e1 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -294,7 +294,6 @@ export class CollectionStackingView extends CollectionSubView { - console.log(doc.title); if (i === 0) { if (targInd === -1) targInd = docs.length; else targInd = docs.indexOf(this.filteredChildren[targInd]); diff --git a/src/fields/List.ts b/src/fields/List.ts index ceb538b2d..a0cbebaf5 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -43,7 +43,7 @@ const listHandlers: any = { } } const res = list.__fields.push(...items); - this[Update]({ op: "$addToSet", items }); + this[Update]({ op: "$addToSet", items, length: length + items.length }); return res; }), reverse() { @@ -77,7 +77,8 @@ const listHandlers: any = { } } const res = list.__fields.splice(start, deleteCount, ...items); - this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed } : undefined); + this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } : + items.length && !deleteCount ? { op: "$addToSet", items, length: list.__fields.length } : undefined); return res.map(toRealField); }), unshift(...items: any[]) { diff --git a/src/fields/util.ts b/src/fields/util.ts index 791b98b83..fd409a54e 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset, DocListCastAsync } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -8,10 +8,13 @@ import { action, trace, observable, reaction, computed } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; -import { ScriptCast, StrCast } from "./Types"; +import { ScriptCast, StrCast, DateCast, Cast, NumCast } from "./Types"; import { returnZero } from "../Utils"; import CursorField from "./CursorField"; import { List } from "./List"; +import { listSpec } from "./Schema"; +import { DateField } from "./DateField"; +import { SnappingManager } from "../client/util/SnappingManager"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -22,32 +25,6 @@ export function TraceMobx() { tracing && trace(); } -// the list of groups that the current user is a member of -export class UserGroups { - static computing = false; - static cachedGroups: string[] = []; - static globalGroupDoc: Doc | undefined; - static get Current() { - if (!Doc.UserDoc() || UserGroups.computing) return UserGroups.cachedGroups; - UserGroups.computing = true; - if (!UserGroups.globalGroupDoc) UserGroups.globalGroupDoc = Doc.UserDoc().globalGroupDatabase as Doc; - if (UserGroups.globalGroupDoc) { - const dbgroups = DocListCast(UserGroups.globalGroupDoc.data); - if (dbgroups.length !== UserGroups.cachedGroups.length - 1) { - UserGroups.cachedGroups = [ - "Public", - ...dbgroups?. - filter(group => group instanceof Doc). - map(group => group as Doc). - filter(group => JSON.parse(StrCast(group.members))?.includes(Doc.CurrentUserEmail)). - map(group => StrCast(group.title)) - ]; - } - } - UserGroups.computing = false; - return UserGroups.cachedGroups; - } -} export interface GetterResult { value: FieldResult; @@ -190,7 +167,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, // if the current user is the author of the document / the current user is a member of the admin group const userChecked = user || Doc.CurrentUserEmail; if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; - if (UserGroups.Current?.includes("Admin")) return AclAdmin; + if (SnappingManager.GetCachedGroups().includes("Admin")) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { @@ -211,7 +188,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group - if (UserGroups.Current?.includes(entity) || userChecked === entity) { + if (SnappingManager.GetCachedGroups().includes(entity) || userChecked === entity) { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; if (effectiveAcl === AclAdmin) return effectiveAcl; @@ -388,9 +365,12 @@ export function deleteProperty(target: any, prop: string | number | symbol) { export function updateFunction(target: any, prop: any, value: any, receiver: any) { let current = ObjectField.MakeCopy(value); return (diff?: any) => { - const op = diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List(diff.items)) } } : - diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List(diff.items)) } } - : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; + const op = + diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List(diff.items)) } } : + diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List(diff.items)) } } + : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; + !op['$set'] && ((op as any).length = diff.length); + const oldValue = current; const newValue = ObjectField.MakeCopy(value); current = newValue; diff --git a/src/server/websocket.ts b/src/server/websocket.ts index c9b5d1cbf..03795d254 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -277,7 +277,8 @@ export namespace WebSocket { const newListItems = diff.diff.$set[updatefield].fields; const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : false))]; - const sendBack = true;//curList.length !== prelen; + const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; + delete diff.diff.length; Database.Instance.update(diff.id, diff.diff, () => { if (sendBack) { @@ -295,7 +296,8 @@ export namespace WebSocket { const remListItems = diff.diff.$set[updatefield].fields; const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : false)); - const sendBack = true;//curList.length + remListItems.length !== prelen; + const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; + delete diff.diff.length; Database.Instance.update(diff.id, diff.diff, () => { if (sendBack) { -- cgit v1.2.3-70-g09d2 From d123cac247bcc37aa00fe44a4933df86b71e93af Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 10 Oct 2020 17:49:41 -0400 Subject: split LinkDatbase into its own field in the user's registry. fixed sharing to set lastModified after modifying groups so that remote participants see the right data. fixed text boxes to show blue icon when there's an annotaitn entry --- src/client/util/CurrentUserUtils.ts | 26 ++++++++++++++-------- src/client/util/GroupManager.tsx | 8 +++---- src/client/util/LinkManager.ts | 12 ++++++---- src/client/util/SharingManager.tsx | 9 +++++--- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +++++----- src/fields/Doc.ts | 1 + src/server/ApiManagers/UserManager.ts | 12 +++++++--- src/server/GarbageCollector.ts | 2 +- src/server/authentication/AuthenticationManager.ts | 1 + src/server/authentication/DashUserModel.ts | 4 +++- 10 files changed, 57 insertions(+), 30 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 694982fea..7535d7c24 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"; @@ -541,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, @@ -876,7 +876,16 @@ 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).data = new List([]); + (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) { @@ -956,7 +965,7 @@ 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), @@ -996,9 +1005,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); + await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId); if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); - if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered @@ -1036,10 +1044,10 @@ export class CurrentUserUtils { public static async loadUserDocument(id: string) { this.curr_id = id; 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/GroupManager.tsx b/src/client/util/GroupManager.tsx index cc1d45a58..6458de0ed 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -145,8 +145,8 @@ export class GroupManager extends React.Component<{}> { */ addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { - this.GroupManagerDoc.lastModified = new DateField; Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc); + this.GroupManagerDoc.lastModified = new DateField; return true; } return false; @@ -160,7 +160,6 @@ export class GroupManager extends React.Component<{}> { deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { - this.GroupManagerDoc.lastModified = new DateField; Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); SharingManager.Instance.removeGroup(group); const members = JSON.parse(StrCast(group.members)); @@ -168,6 +167,7 @@ export class GroupManager extends React.Component<{}> { 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) { this.currentGroup = undefined; } @@ -187,8 +187,8 @@ export class GroupManager extends React.Component<{}> { const memberList = JSON.parse(StrCast(groupDoc.members)); !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); - this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); SharingManager.Instance.shareWithAddedMember(groupDoc, email); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); } } @@ -204,8 +204,8 @@ export class GroupManager extends React.Component<{}> { if (index !== -1) { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); - this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); SharingManager.Instance.removeMember(groupDoc, email); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); } } } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 56b6cb8a9..0456b4029 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -36,17 +36,21 @@ export class LinkManager { public getAllLinks(): Doc[] { - const lset = new Set(DocListCast(Doc.UserDoc().myLinkDatabase)); - SharingManager.Instance.users.forEach(user => DocListCast(user.sharingDoc.myLinkDatabase).map(lset.add)); + const lset = new Set(DocListCast(Doc.LinkDBDoc().data)); + SharingManager.Instance.users.forEach(user => { + DocListCast((user.linkDatabase as Doc)?.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 diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 16bcd46c8..271face98 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -22,11 +22,11 @@ import "./SharingManager.scss"; import { SelectionManager } from "./SelectionManager"; import { intersection } from "lodash"; import { SearchBox } from "../views/search/SearchBox"; -import { listSpec } from "../../fields/Schema"; export interface User { email: string; sharingDocumentId: string; + linkDatabaseId: string; } /** @@ -53,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? } @@ -130,8 +131,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) }); } } }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 598657a58..99a009d13 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -289,10 +289,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } else { - - const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!); - json.selection = state.toJSON().selection; - this._editorView.updateState(EditorState.fromJSON(this.config, json)); + const jsonstring = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!; + if (jsonstring) { + const json = JSON.parse(jsonstring); + json.selection = state.toJSON().selection; + this._editorView.updateState(EditorState.fromJSON(this.config, json)); + } } } } @@ -1554,7 +1556,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } @computed get sidebarHandle() { - const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.title).length; + const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.author).length; return !this.props.isSelected() && !(annotated && !this.sidebarWidth()) ? (null) :
manager._searchQuery = query); } export function UserDoc(): Doc { return manager._user_doc; } export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); } + export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); } export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; } export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; } diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index e5c0f3827..f36506b14 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -19,9 +19,9 @@ export default class UserManager extends ApiManager { method: Method.GET, subscription: "/getUsers", secureHandler: async ({ res }) => { - const cursor = await Database.Instance.query({}, { email: 1, sharingDocumentId: 1 }, "users"); + const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, "users"); const results = await cursor.toArray(); - res.send(results.map(user => ({ email: user.email, sharingDocumentId: user.sharingDocumentId }))); + res.send(results.map(user => ({ email: user.email, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }))); } }); @@ -47,7 +47,7 @@ export default class UserManager extends ApiManager { register({ method: Method.GET, subscription: "/getUserDocumentIds", - secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, sharingDocumentId: user.sharingDocumentId }) + secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }) }); register({ @@ -56,6 +56,12 @@ export default class UserManager extends ApiManager { secureHandler: ({ res, user }) => res.send(user.sharingDocumentId) }); + register({ + method: Method.GET, + subscription: "/getLinkDatabaseId", + secureHandler: ({ res, user }) => res.send(user.linkDatabaseId) + }); + register({ method: Method.GET, subscription: "/getCurrentUser", diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 6bd0e5163..7c441e3c0 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -65,7 +65,7 @@ async function GarbageCollect(full: boolean = true) { // await new Promise(res => setTimeout(res, 3000)); const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users'); const users = await cursor.toArray(); - const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId)]]; + const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId), ...users.map(user => user.linkDatabaseId)]; const visited = new Set(); const files: { [name: string]: string[] } = {}; diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts index 84abd41a2..9eb4a328f 100644 --- a/src/server/authentication/AuthenticationManager.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -49,6 +49,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { password, userDocumentId: Utils.GenerateGuid(), sharingDocumentId: Utils.GenerateGuid(), + linkDatabaseId: Utils.GenerateGuid(), cacheDocumentIds: "" } as Partial; diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index 4f2856a78..bee28b96d 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -11,6 +11,7 @@ export type DashUserModel = mongoose.Document & { userDocumentId: string; sharingDocumentId: string; + linkDatabaseId: string; cacheDocumentIds: string; profile: { @@ -39,7 +40,8 @@ const userSchema = new mongoose.Schema({ userDocumentId: String, // id that identifies a document which hosts all of a user's account data sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users - cacheDocumentIds: String, + linkDatabaseId: String, + cacheDocumentIds: String, // set of document ids to retreive on startup facebook: String, twitter: String, -- cgit v1.2.3-70-g09d2 From c63aeeb0010a79f0b19d8719f97d98b7a83baf3b Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 10 Oct 2020 20:24:41 -0400 Subject: cached groups user is in for efficiency which avoids th whole screen redrawing when a group is added. --- src/client/util/LinkManager.ts | 4 +--- src/client/util/SharingManager.tsx | 4 ++-- src/client/util/SnappingManager.ts | 5 ++++- src/fields/util.ts | 8 +++----- 4 files changed, 10 insertions(+), 11 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 0456b4029..e74ce87f8 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"; @@ -71,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/SharingManager.tsx b/src/client/util/SharingManager.tsx index 271face98..0c8f19eae 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -473,7 +473,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-"; @@ -489,7 +489,7 @@ export class SharingManager extends React.Component<{}> { diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index d067dff6c..a615f0247 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 { @@ -32,6 +33,8 @@ export namespace SnappingManager { /// 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. export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); } - export function GetCachedGroups() { return manager.cachedGroups; } + export function GetCachedGroupByName(name: string) { + return computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true)(name); + } } diff --git a/src/fields/util.ts b/src/fields/util.ts index fd409a54e..881f301f3 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -12,8 +12,6 @@ import { ScriptCast, StrCast, DateCast, Cast, NumCast } from "./Types"; import { returnZero } from "../Utils"; import CursorField from "./CursorField"; import { List } from "./List"; -import { listSpec } from "./Schema"; -import { DateField } from "./DateField"; import { SnappingManager } from "../client/util/SnappingManager"; function _readOnlySetter(): never { @@ -167,7 +165,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, // if the current user is the author of the document / the current user is a member of the admin group const userChecked = user || Doc.CurrentUserEmail; if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; - if (SnappingManager.GetCachedGroups().includes("Admin")) return AclAdmin; + if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { @@ -188,7 +186,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group - if (SnappingManager.GetCachedGroups().includes(entity) || userChecked === entity) { + if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; if (effectiveAcl === AclAdmin) return effectiveAcl; @@ -369,7 +367,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List(diff.items)) } } : diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List(diff.items)) } } : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; - !op['$set'] && ((op as any).length = diff.length); + !op.$set && ((op as any).length = diff.length); const oldValue = current; const newValue = ObjectField.MakeCopy(value); -- cgit v1.2.3-70-g09d2 From 858fdbf31c9ddbf00eae692cd43e70455a19e1b5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 10 Oct 2020 20:49:26 -0400 Subject: fixed docFilters to use up one string field, not 3. makes adding/removing to lists incrementally work with new server code. --- src/client/documents/Documents.ts | 14 ++++++++++---- src/client/util/LinkManager.ts | 2 +- .../views/collections/CollectionSchemaHeaders.tsx | 14 ++++++++------ src/client/views/nodes/FilterBox.tsx | 16 ++++++++-------- src/client/views/search/SearchBox.tsx | 20 +++++++++++--------- src/fields/Doc.ts | 13 ++++++------- src/server/websocket.ts | 4 ++-- 7 files changed, 46 insertions(+), 37 deletions(-) (limited to 'src/client/util') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7d78bd76a..159771145 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -895,8 +895,11 @@ export namespace Docs { export namespace DocUtils { export function Excluded(d: Doc, docFilters: string[]) { const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); + for (let i = 0; i < docFilters.length; i++) { + const fields = docFilters[i].split(":"); + const key = fields[0]; + const value = fields[1]; + const modifiers = fields[2]; if (!filterFacets[key]) { filterFacets[key] = {}; } @@ -918,8 +921,11 @@ export namespace DocUtils { const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); + for (let i = 0; i < docFilters.length; i++) { + const fields = docFilters[i].split(":"); + const key = fields[0]; + const value = fields[1]; + const modifiers = fields[2]; if (!filterFacets[key]) { filterFacets[key] = {}; } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index e74ce87f8..1ba6cff6d 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -36,7 +36,7 @@ export class LinkManager { public getAllLinks(): Doc[] { const lset = new Set(DocListCast(Doc.LinkDBDoc().data)); SharingManager.Instance.users.forEach(user => { - DocListCast((user.linkDatabase as Doc)?.data).map(doc => { + DocListCast(user.linkDatabase?.data).map(doc => { lset.add(doc); }); }); diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index dbf7488ec..b408fd680 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -261,7 +261,7 @@ export class KeysDropdown extends React.Component { componentDidMount() { document.addEventListener("pointerdown", this.detectClick); const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters?.includes(this._key)) { + if (filters?.some(filter => filter.split(":")[0] === this._key)) { runInAction(() => this.closeResultsVisibility = "contents"); } } @@ -396,19 +396,21 @@ export class KeysDropdown extends React.Component { }); const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { this.props.col.setColor("rgb(241, 239, 235)"); this.closeResultsVisibility = "none"; } - for (let i = 0; i < (filters?.length ?? 0) - 1; i += 3) { - if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i + 1]) === false) { + for (let i = 0; i < (filters?.length ?? 0) - 1; i++) { + if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i].split(":")[1]) === false) { keyOptions.push(filters![i + 1]); } } const options = keyOptions.map(key => { let bool = false; if (filters !== undefined) { - bool = filters.includes(key) && filters[filters.indexOf(key) + 1] === "check"; + const ind = filters.findIndex(filter => filter.split(":")[0] === key); + const fields = ind === -1 ? undefined : filters[ind].split(":"); + bool = fields ? fields[1] === "check" : false; } return
{ updateFilter() { const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { this.props.col.setColor("rgb(241, 239, 235)"); this.closeResultsVisibility = "none"; } diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index d3c0bfba3..0d02a4388 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -94,15 +94,15 @@ export class FilterBox extends ViewBoxBaseComponent item === facetHeader)) !== -1) { - docFilter.splice(index, 3); + while ((index = docFilter.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + docFilter.splice(index, 1); } } const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); if (docRangeFilters) { let index: number; - while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { - docRangeFilters.splice(index, 3); + while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + docRangeFilters.splice(index, 1); } } } else { @@ -224,10 +224,10 @@ export class FilterBox extends ViewBoxBaseComponent { - for (let i = 0; i < docFilters.length; i += 3) { - if (docFilters[i] === key && (docFilters[i + 1] === value || modifiers === "match" || modifiers === "remove")) { - if (docFilters[i + 2] === modifiers && modifiers && docFilters[i + 1] === value) return; - docFilters.splice(i, 3); + for (let i = 0; i < docFilters.length; i++) { + const fields = docFilters[i].split(":"); // split key:value:modifier + if (fields[0] === key && (fields[1] === value || modifiers === "match" || modifiers === "remove")) { + if (fields[2] === modifiers && modifiers && fields[1] === value) return; + docFilters.splice(i, 1); container._docFilters = new List(docFilters); break; } @@ -1065,9 +1066,7 @@ export namespace Doc { if (!docFilters.length && modifiers === "match" && value === undefined) { container._docFilters = undefined; } else if (modifiers !== "remove") { - docFilters.push(key); - docFilters.push(value); - docFilters.push(modifiers); + docFilters.push(key + ":" + value + ":" + modifiers); container._docFilters = new List(docFilters); } } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 03795d254..1e02b9e58 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -276,7 +276,7 @@ export namespace WebSocket { const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const newListItems = diff.diff.$set[updatefield].fields; const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; - diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : false))]; + diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !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, @@ -295,7 +295,7 @@ export namespace WebSocket { const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const remListItems = diff.diff.$set[updatefield].fields; const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; - diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : false)); + diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)); const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; Database.Instance.update(diff.id, diff.diff, -- cgit v1.2.3-70-g09d2 From 27635402ad810b910557eb1a86c7e85fa281aaee Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 12 Oct 2020 01:05:13 -0400 Subject: switched fonticonbox and colelctionsubview to test GetEffectiveAcl to determine if document is available (instead of hack of testing 'author'). made GetEffectiveAcl a computedFn. No longer create a pushpin when an annotation that's linked to text is dragged off a PDF. fixed undo of pushpin navigation (used to call finish() twice). fixed pushpin navigation to conistenly pan & sensibly toggle target --- src/client/util/DocumentManager.ts | 9 +++++---- src/client/views/collections/CollectionSubView.tsx | 5 +++-- src/client/views/collections/CollectionView.tsx | 3 ++- .../collectionFreeForm/CollectionFreeFormView.tsx | 21 +++++++++++++-------- src/client/views/nodes/FontIconBox.tsx | 5 +++-- src/client/views/pdf/Annotation.tsx | 2 ++ src/client/views/pdf/PDFViewer.tsx | 2 +- src/fields/util.ts | 14 ++++++++++---- 8 files changed, 39 insertions(+), 22 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 178daf5f0..b37181e57 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); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 493018093..b282d1e27 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -103,7 +103,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? 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.proto || (pair.layout.proto instanceof Doc && Object.keys(pair.layout.proto).length)); + return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length)); }); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } @@ -134,7 +134,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : []; } - const docs = rawdocs.filter(d => !(d instanceof Promise) && Object.keys(d).length).map(d => d as Doc); + const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(d) !== AclPrivate).map(d => d as Doc); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; @@ -502,4 +502,5 @@ import { SelectionManager } from "../../util/SelectionManager"; import { OverlayView } from "../OverlayView"; import { setTimeout } from "timers"; import { Hypothesis } from "../../util/HypothesisUtils"; +import { GetEffectiveAcl } from "../../../fields/util"; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 80e9b41ad..cfd24545b 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -159,7 +159,8 @@ export class CollectionView extends Touchable [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => { // only make a pushpin if we have acl's to edit the document const context = Cast(doc.context, Doc, null); - if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { + const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); + if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ title: "pushpin", label: "", icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4d9906f93..4df90e8ea 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -893,21 +893,24 @@ export class CollectionFreeFormView extends CollectionSubView NumCast(doc.y)) { - scrollTo = NumCast(doc.y); + scrollTo = Math.max(0, NumCast(doc.y) - 50); } - if (curScroll !== scrollTo) { + if (curScroll !== scrollTo || this.props.Document._viewTransition) { this.props.Document._scrollPY = this.props.Document._scrollY = scrollTo; delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0; - !dontCenter && delay && this.props.focus(this.props.Document); + !dontCenter && this.props.focus(this.props.Document); afterFocus && setTimeout(afterFocus, delay); + } else { + !dontCenter && delay && this.props.focus(this.props.Document); // @ts-ignore - } else afterFocus(true); // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll + afterFocus(true); // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll + + } } } else { @@ -929,14 +932,16 @@ export class CollectionFreeFormView extends CollectionSubView { - if (afterFocus?.()) { + // @ts-ignore + if (afterFocus?.(notFocused)) { // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll this.Document._panX = savedState.px; this.Document._panY = savedState.py; this.Document[this.scaleFieldKey] = savedState.s; this.Document._viewTransition = savedState.pt; } - }, 500); + }, notFocused ? 0 : 500); } } diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 156256fe5..276c66bb1 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -8,11 +8,12 @@ import { FieldView, FieldViewProps } from './FieldView'; import { StrCast, Cast, ScriptCast } from '../../../fields/Types'; import { Utils, setupMoveUpEvents, returnFalse, emptyFunction } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, AclPrivate } from '../../../fields/Doc'; import { ContextMenu } from '../ContextMenu'; import { ScriptField } from '../../../fields/ScriptField'; import { Tooltip } from '@material-ui/core'; import { DragManager } from '../../util/DragManager'; +import { GetEffectiveAcl } from '../../../fields/util'; const FontIconSchema = createSchema({ icon: "string", }); @@ -105,7 +106,7 @@ export class FontIconBadge extends React.Component { render() { if (!(this.props.collection instanceof Doc)) return (null); - const length = DocListCast(this.props.collection.data).filter(d => Object.keys(d).length).length; // filter out any documents that we can't read + const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read return
0 ? { "display": "initial" } : { "display": "none" }} onPointerDown={this.onPointerDown} > diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index e7f901091..a071abd21 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -71,6 +71,7 @@ class RegionAnnotation extends React.Component { this._reactionDisposer && this._reactionDisposer(); } + @undoBatch deleteAnnotation = () => { const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]); const group = FieldValue(Cast(this.props.document.group, Doc)); @@ -86,6 +87,7 @@ class RegionAnnotation extends React.Component { PDFMenu.Instance.fadeOut(true); } + @undoBatch pinToPres = () => { const group = FieldValue(Cast(this.props.document.group, Doc)); const isPinned = group && Doc.isDocPinned(group) ? true : false; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 78b95b385..d8be3defd 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -194,7 +194,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent= 0) { - if (!this._mainCont.current) setTimeout(() => smoothScroll(1000, this._mainCont.current!, scrollY || 0)); + if (!this._mainCont.current) setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0)); else smoothScroll(1000, this._mainCont.current, scrollY || 0); this.Document._scrollPY = undefined; } diff --git a/src/fields/util.ts b/src/fields/util.ts index 881f301f3..b68d961b1 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -13,6 +13,7 @@ import { returnZero } from "../Utils"; import CursorField from "./CursorField"; import { List } from "./List"; import { SnappingManager } from "../client/util/SnappingManager"; +import { computedFn } from "mobx-utils"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -139,7 +140,7 @@ export function denormalizeEmail(email: string) { * * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. * - * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything. + * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. * * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. * @@ -148,7 +149,7 @@ export function denormalizeEmail(email: string) { export enum SharingPermissions { Admin = "Admin", Edit = "Can Edit", - Add = "Can Add", + Add = "Can Augment", View = "Can View", None = "Not Shared" } @@ -157,6 +158,11 @@ export enum SharingPermissions { * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { + return computedFn(function (target: any, in_prop?: string | symbol | number, user?: string) { + return getEffectiveAcl(target, in_prop, user); + }, true)(target, in_prop, user); +} +function getEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { if (!target) return AclPrivate; // all changes received fromt the server must be processed as Admin @@ -219,7 +225,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc const HierarchyMapping = new Map([ ["Not Shared", 0], ["Can View", 1], - ["Can Add", 2], + ["Can Augment", 2], ["Can Edit", 3], ["Admin", 4] ]); @@ -281,7 +287,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an // 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 Add", "Can View", "Not Shared", undefined].includes(value)) return true; + // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { -- cgit v1.2.3-70-g09d2 From d1959f141a777397bd5eeb5d40bcfe7195538557 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 12 Oct 2020 10:23:29 -0400 Subject: fixed computedFn()'s to be used correctly in several places. fixed major memory leak in PDFs --- src/client/util/SelectionManager.ts | 8 ++++---- src/client/util/SnappingManager.ts | 5 ++--- src/client/views/nodes/PDFBox.tsx | 31 ++++++++++++++++++------------- src/fields/Doc.ts | 19 ++++++++----------- src/fields/util.ts | 20 ++++++++++---------- 5 files changed, 42 insertions(+), 41 deletions(-) (limited to 'src/client/util') 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/SnappingManager.ts b/src/client/util/SnappingManager.ts index a615f0247..069f81d38 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -32,9 +32,8 @@ export namespace SnappingManager { /// 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); } - export function GetCachedGroupByName(name: string) { - return computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true)(name); - } } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 74b431bea..79e00eed7 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -35,7 +35,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent(); private _keyRef = React.createRef(); @@ -44,7 +44,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent; @observable private _pageControls = false; @@ -54,6 +53,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent this._pdf = PDFBox.pdfcache.get(pdfUrl.url.href)); + else if (PDFBox.pdfpromise.get(pdfUrl.url.href)) PDFBox.pdfpromise.get(pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf)); + } const backup = "oldPath"; const { Document } = this.props; @@ -265,22 +269,23 @@ export class PDFBox extends ViewBoxAnnotatableComponent; } - _pdfjsRequested = false; + static pdfcache = new Map(); + static pdfpromise = new Map>(); render() { TraceMobx(); - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0) || this.props.Document._scrollY !== undefined) { - this._everActive = true; + this._displayPdfLive = true; } - if (pdfUrl && this._everActive) { - if (pdfUrl instanceof PdfField && this._pdf) { - return this.renderPdfView; - } - if (!this._pdfjsRequested) { - this._pdfjsRequested = true; - const promise = Pdfjs.getDocument(pdfUrl.url.href).promise; - promise.then(action(pdf => this._pdf = pdf)); + if (this._displayPdfLive) { + if (this._pdf) return this.renderPdfView; + const href = Cast(this.dataDoc[this.props.fieldKey], PdfField, null)?.url.href; + if (href) { + if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href))); + else { + if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); + PDFBox.pdfpromise.get(href)?.then(action(pdf => PDFBox.pdfcache.set(href, this._pdf = pdf))); + } } } return this.renderTitleBox; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 086b7777f..2452ab408 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -891,12 +891,11 @@ export namespace Doc { export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; } export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); } - export function IsSearchMatch(doc: Doc) { - return computedFn(function IsSearchMatch(doc: Doc) { - return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : - brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; - })(doc); - } + const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) { + return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : + brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; + }); + export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); } export function IsSearchMatchUnmemoized(doc: Doc) { return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; @@ -918,11 +917,9 @@ export namespace Doc { brushManager.SearchMatchDoc.clear(); } - export function IsBrushed(doc: Doc) { - return computedFn(function IsBrushed(doc: Doc) { - return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); - })(doc); - } + const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); }); + export function IsBrushed(doc: Doc) { return isBrushedCache(doc); } + // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) export function IsBrushedDegreeUnmemoized(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0; diff --git a/src/fields/util.ts b/src/fields/util.ts index b68d961b1..5cd8df564 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -154,30 +154,30 @@ export enum SharingPermissions { None = "Not Shared" } +// return acl from cache or cache the acl and return. +const getEffectiveAclCache = computedFn(function (target: any, playgroundProp: boolean, user?: string) { return getEffectiveAcl(target, playgroundProp, user); }, true); + /** * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { - return computedFn(function (target: any, in_prop?: string | symbol | number, user?: string) { - return getEffectiveAcl(target, in_prop, user); - }, true)(target, in_prop, user); -} -function getEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { if (!target) return AclPrivate; + if (in_prop === UpdatingFromServer) return AclAdmin; // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent + const playgroundProp = in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()) ? true : false; + return getEffectiveAclCache(target, playgroundProp, user); +} - // all changes received fromt the server must be processed as Admin - if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; - +function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): symbol { + if (target[UpdatingFromServer]) return AclAdmin; // all changes received from the server must be processed as Admin // if the current user is the author of the document / the current user is a member of the admin group const userChecked = user || Doc.CurrentUserEmail; if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; - if (target[AclSym] && Object.keys(target[AclSym]).length) { // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified) - if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; + if (_overrideAcl || playgroundProp) return AclEdit; let effectiveAcl = AclPrivate; const HierarchyMapping = new Map([ -- cgit v1.2.3-70-g09d2 From 04bbff759710d58fa97ce2f0d685ec59b6beb60e Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 12 Oct 2020 12:53:40 -0400 Subject: split getEffectiveAcl code for determining if a field can be modified out into getPropAcl. --- src/client/util/SharingManager.tsx | 4 +-- src/client/views/DocComponent.tsx | 2 +- src/client/views/collections/CollectionView.tsx | 2 +- src/fields/util.ts | 44 ++++++++++++++----------- 4 files changed, 28 insertions(+), 24 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 0c8f19eae..914253e3c 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -177,7 +177,7 @@ export class SharingManager extends React.Component<{}> { users.forEach(({ user, sharingDoc }) => { 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, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists + 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 }); } }); @@ -273,7 +273,7 @@ export class SharingManager extends React.Component<{}> { distributeAcls(acl, permission as SharingPermissions, doc); if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); - else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); }); } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 98e888538..a55f4adaf 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -164,7 +164,7 @@ export function ViewBoxAnnotatableComponent

{ for (const [key, value] of Object.entries(this.props.Document[AclSym])) { if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index cfd24545b..a27fa5a66 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -140,7 +140,7 @@ export class CollectionView extends Touchable { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); diff --git a/src/fields/util.ts b/src/fields/util.ts index 56736028a..dd0444d61 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -155,32 +155,35 @@ export enum SharingPermissions { } // return acl from cache or cache the acl and return. bcz: Argh! NOT WORKING ... nothing gets invalidated properly.... -const getEffectiveAclCache = computedFn(function (target: any, playgroundProp: boolean, user?: string) { return getEffectiveAcl(target, playgroundProp, user); }, true); +const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true); /** * Calculates the effective access right to a document for the current user. */ -export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { - if (!target) return AclPrivate; - if (in_prop === UpdatingFromServer) return AclAdmin; // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent - const playgroundProp = in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()) ? true : false; - return getEffectiveAcl(target, playgroundProp, user); +export function GetEffectiveAcl(target: any, user?: string): symbol { + return target ? getEffectiveAcl(target, user) : AclPrivate; } -function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): symbol { +function getPropAcl(target: any, prop: string | symbol | number) { + if (prop === UpdatingFromServer) return AclAdmin; // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent + if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable + return GetEffectiveAcl(target); +} + +let HierarchyMapping: Map | undefined; + +function getEffectiveAcl(target: any, user?: string): symbol { if (target[UpdatingFromServer]) return AclAdmin; // all changes received from the server must be processed as Admin // if the current user is the author of the document / the current user is a member of the admin group const userChecked = user || Doc.CurrentUserEmail; - if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; + if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; + const targetAcls = target[AclSym]; - if (target[AclSym] && Object.keys(target[AclSym]).length) { + if (targetAcls && Object.keys(targetAcls).length) { + if (_overrideAcl) return AclEdit; - // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified) - if (_overrideAcl || playgroundProp) return AclEdit; - - let effectiveAcl = AclPrivate; - const HierarchyMapping = new Map([ + HierarchyMapping = HierarchyMapping || new Map([ [AclPrivate, 0], [AclReadonly, 1], [AclAddonly, 2], @@ -188,12 +191,13 @@ function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): s [AclAdmin, 4] ]); - for (const [key, value] of Object.entries(target[AclSym])) { + let effectiveAcl = AclPrivate; + for (const [key, value] of Object.entries(targetAcls)) { // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group - if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { - if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { + if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { + if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { effectiveAcl = value as symbol; if (effectiveAcl === AclAdmin) return effectiveAcl; } @@ -201,8 +205,8 @@ function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): s } // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document - const override = target[AclSym]["acl-Override"]; - if (override !== AclUnset && override !== undefined) effectiveAcl = target[AclSym]["acl-Override"]; + const override = targetAcls["acl-Override"]; + if (override !== AclUnset && override !== undefined) effectiveAcl = override; // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl; @@ -282,7 +286,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - const effectiveAcl = GetEffectiveAcl(target, in_prop); + const effectiveAcl = getPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't -- cgit v1.2.3-70-g09d2 From 8ebf3cb0ac7a023aa47a5264d74c3edaebf28b1b Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 12 Oct 2020 17:22:26 -0400 Subject: updated getEffectiveAcl to be a computedFn. got rid of OverrideAcl in favor of just using UpdatingFromServer --- src/client/DocServer.ts | 2 +- src/client/util/SerializationHelper.ts | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 8 ++--- src/fields/Doc.ts | 20 +++++++----- src/fields/util.ts | 36 +++++++++------------- 6 files changed, 33 insertions(+), 37 deletions(-) (limited to 'src/client/util') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index d936f6e2a..00f9877c3 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, fetchProto, FieldsSym, UpdatingFromServer } from '../fields/Doc'; +import { Opt, Doc, UpdatingFromServer } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; 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/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index b282d1e27..f3e563422 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -134,7 +134,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : []; } - const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(d) !== AclPrivate).map(d => d as Doc); + const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index da797eda0..4f8f46111 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,7 +11,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin, UpdatingFromServer } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; import applyDevTools = require("prosemirror-dev-tools"); import { removeMarkWithAttrs } from "./prosemirrorPatches"; @@ -22,7 +22,7 @@ import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { makeInterface } from "../../../../fields/Schema"; import { Cast, DateCast, NumCast, StrCast, ScriptCast, BoolCast } from "../../../../fields/Types"; -import { TraceMobx, OVERRIDE_acl, GetEffectiveAcl } from '../../../../fields/util'; +import { TraceMobx, GetEffectiveAcl } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents, OmitKeys } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../../DocServer"; @@ -805,9 +805,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp tr = tr.addMark(pos, pos + node.nodeSize, link); } }); - OVERRIDE_acl(true); + this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); - OVERRIDE_acl(false); + this.dataDoc[UpdatingFromServer] = false; } } componentDidMount() { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 2452ab408..cea09b9c5 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -102,22 +102,28 @@ const AclMap = new Map([ [SharingPermissions.Admin, AclAdmin] ]); -export function fetchProto(doc: Doc) { +// caches the document access permissions for the current user. +// this recursively updates all protos as well. +export function updateCachedAcls(doc: Doc) { if (!doc) return; const permissions: { [key: string]: symbol } = {}; + doc[UpdatingFromServer] = true; Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); + doc[UpdatingFromServer] = false; - if (Object.keys(permissions).length) doc[AclSym] = permissions; + if (Object.keys(permissions).length) { + doc[AclSym] = permissions; + } if (doc.proto instanceof Promise) { - doc.proto.then(fetchProto); + doc.proto.then(updateCachedAcls); return doc.proto; } } @scriptingGlobal -@Deserializable("Doc", fetchProto).withFields(["id"]) +@Deserializable("Doc", updateCachedAcls).withFields(["id"]) export class Doc extends RefField { constructor(id?: FieldId, forceSave?: boolean) { super(id); @@ -233,17 +239,15 @@ export class Doc extends RefField { const prev = GetEffectiveAcl(this); this[UpdatingFromServer] = true; this[fKey] = value; + this[UpdatingFromServer] = false; if (fKey.startsWith("acl")) { - fetchProto(this); + updateCachedAcls(this); } - this[UpdatingFromServer] = false; if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { DocServer.GetRefField(this[Id], true); } // if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) { - // this[UpdatingFromServer] = true; // this[FieldsSym](true); - // this[UpdatingFromServer] = false; // } }; if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { diff --git a/src/fields/util.ts b/src/fields/util.ts index dd0444d61..d48011194 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,14 +1,14 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset, DocListCastAsync } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; -import { action, trace, observable, reaction, computed } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols"; +import { action, trace, } from "mobx"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; -import { ScriptCast, StrCast, DateCast, Cast, NumCast } from "./Types"; +import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; import CursorField from "./CursorField"; import { List } from "./List"; @@ -114,10 +114,6 @@ export function makeReadOnly() { export function makeEditable() { _setter = _setterImpl; } -var _overrideAcl = false; -export function OVERRIDE_acl(val: boolean) { - _overrideAcl = val; -} export function normalizeEmail(email: string) { return email.replace(/\./g, '__'); @@ -154,18 +150,19 @@ export enum SharingPermissions { None = "Not Shared" } -// return acl from cache or cache the acl and return. bcz: Argh! NOT WORKING ... nothing gets invalidated properly.... +// return acl from cache or cache the acl and return. const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true); /** * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, user?: string): symbol { - return target ? getEffectiveAcl(target, user) : AclPrivate; + return !target ? AclPrivate : + target[UpdatingFromServer] ? AclAdmin : getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } function getPropAcl(target: any, prop: string | symbol | number) { - if (prop === UpdatingFromServer) return AclAdmin; // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent + if (prop === UpdatingFromServer || 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 return GetEffectiveAcl(target); } @@ -173,16 +170,12 @@ function getPropAcl(target: any, prop: string | symbol | number) { let HierarchyMapping: Map | undefined; function getEffectiveAcl(target: any, user?: string): symbol { - if (target[UpdatingFromServer]) return AclAdmin; // all changes received from the server must be processed as Admin - // if the current user is the author of the document / the current user is a member of the admin group - const userChecked = user || Doc.CurrentUserEmail; + const targetAcls = target[AclSym]; + const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; - const targetAcls = target[AclSym]; if (targetAcls && Object.keys(targetAcls).length) { - if (_overrideAcl) return AclEdit; - HierarchyMapping = HierarchyMapping || new Map([ [AclPrivate, 0], [AclReadonly, 1], @@ -199,7 +192,6 @@ function getEffectiveAcl(target: any, user?: string): symbol { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { effectiveAcl = value as symbol; - if (effectiveAcl === AclAdmin) return effectiveAcl; } } } @@ -278,8 +270,8 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc }); } - layoutDocChanged && fetchProto(target); // updates target[AclSym] when changes to acls have been made - dataDocChanged && fetchProto(dataDoc); + layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made + dataDocChanged && updateCachedAcls(dataDoc); } const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", @@ -312,9 +304,9 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; + if (in_prop === AclSym) return target[AclSym]; if (in_prop === "toString" || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === "symbol")) return target.__fields[prop] || target[prop]; - if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; + if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; if (prop === LayoutSym) return target.__LAYOUT__; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { -- cgit v1.2.3-70-g09d2 From d87cd4f20a8b5b4f33d799a4ef42fb14337638a5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 13 Oct 2020 14:11:24 -0400 Subject: fixed jumpToDocument where target is in a collection that isn't shown to display the doc in the colleciton without addinga an extra tab for the target. doc itself. --- src/client/util/DocumentManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/util') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index b37181e57..a408e1df6 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -220,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)); } } } -- cgit v1.2.3-70-g09d2 From 79221b73d0526ce31349cf4a5b75f0047c60b829 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 14 Oct 2020 11:02:40 -0400 Subject: fixed focusDocument in freeform views to work with annotation overlays(mostly) - still an issue when original image is in a frame that doen't match its dimensions --- src/client/util/CurrentUserUtils.ts | 1 + src/client/views/collections/TabDocView.tsx | 12 +++++++----- .../collectionFreeForm/CollectionFreeFormView.tsx | 19 +++++++++++-------- src/client/views/nodes/ImageBox.tsx | 16 +++++++++------- 4 files changed, 28 insertions(+), 20 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 7535d7c24..dcbeba8cd 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -881,6 +881,7 @@ export class CurrentUserUtils { 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([]); (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add; } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 70eb5b895..0c7f39dc7 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -45,6 +45,8 @@ export class TabDocView extends React.Component { @observable private _document: Doc | undefined; @observable private _view: DocumentView | undefined; + @computed get contentScaling() { return this.ContentScaling(); } + get stack(): any { return (this.props as any).glContainer.parent.parent; } get tab() { return (this.props as any).glContainer.tab; } get view() { return this._view; } @@ -194,7 +196,7 @@ export class TabDocView extends React.Component { panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; nativeWidth = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeWidth) || this._panelWidth : 0; nativeHeight = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeHeight) || this._panelHeight : 0; - contentScaling = () => { + ContentScaling = () => { const nativeH = NumCast(this.layoutDoc?._nativeHeight); const nativeW = NumCast(this.layoutDoc?._nativeWidth); let scaling = 1; @@ -210,12 +212,12 @@ export class TabDocView extends React.Component { if (this._mainCont?.children) { const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0]?.firstChild as HTMLElement); const scale = Utils.GetScreenTransform(this._mainCont).scale; - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.ContentScaling() / scale); } return Transform.Identity(); } - @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}% ` : undefined; } + @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.ContentScaling()) / 2 : 0; } + @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.ContentScaling()) / this._panelWidth * 100}% ` : undefined; } @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // adds a tab to the layout based on the locaiton parameter which can be: @@ -337,7 +339,7 @@ export class TabDocView extends React.Component { rootSelected={returnTrue} addDocument={undefined} removeDocument={undefined} - ContentScaling={this.contentScaling} + ContentScaling={this.ContentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} NativeHeight={this.nativeHeight() ? this.nativeHeight : undefined} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f075aefe1..8b9e84bd6 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -787,9 +787,12 @@ export class CollectionFreeFormView extends CollectionSubView 20) { deltaScale = 20 / invTransform.Scale; } + if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) { + deltaScale = 1 / invTransform.Scale; + } const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - if ((localTransform.Scale > 1 || !this.props.annotationsKey) && (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling())) { + if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 20); this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); @@ -919,21 +922,21 @@ export class CollectionFreeFormView extends CollectionSubView { - const pw = this.props.PanelWidth(); - const ph = this.props.PanelHeight(); + const pw = this.isAnnotationOverlay ? NumCast(this.props.Document._nativeWidth) : this.props.PanelWidth(); + const ph = this.isAnnotationOverlay ? NumCast(this.props.Document._nativeHeight) : this.props.PanelHeight(); pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height))); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 164456cfb..bc38a17eb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -67,6 +67,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); @@ -266,7 +268,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent { const remoteUrl = this.dataDoc.googlePhotosUrl; return !remoteUrl ? (null) : ( window.open(remoteUrl)} @@ -291,7 +293,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent { const { dataDoc } = this; @@ -402,18 +404,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.props.ContentScaling()); + screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.contentScaling); contentFunc = () => [this.content]; render() { TraceMobx(); return (