diff options
author | bobzel <zzzman@gmail.com> | 2023-01-02 15:05:51 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-02 15:05:51 -0500 |
commit | 9211251c8a632ef84198253a0d3086df11af9ead (patch) | |
tree | 378330aeaecdd68e291dfa0b68c339dfa1c233da /src | |
parent | 4c0de84cf9a3d5be2f5058d514c8ca58e2004a4b (diff) | |
parent | da9aef9e4cda036264801191361c1ecfacd9ba0b (diff) |
Merge pull request #157 from brown-dash/robustness
cleaning up ACLs for performance and clarity
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 1 | ||||
-rw-r--r-- | src/client/util/LinkManager.ts | 8 | ||||
-rw-r--r-- | src/client/util/SerializationHelper.ts | 39 | ||||
-rw-r--r-- | src/client/util/SettingsManager.scss | 16 | ||||
-rw-r--r-- | src/client/util/SettingsManager.tsx | 26 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 56 | ||||
-rw-r--r-- | src/client/views/ContextMenuItem.tsx | 1 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 1 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 33 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 2 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresEnums.ts | 1 | ||||
-rw-r--r-- | src/fields/Doc.ts | 50 | ||||
-rw-r--r-- | src/fields/FieldSymbols.ts | 24 | ||||
-rw-r--r-- | src/fields/List.ts | 5 | ||||
-rw-r--r-- | src/fields/Proxy.ts | 17 | ||||
-rw-r--r-- | src/fields/ScriptField.ts | 16 | ||||
-rw-r--r-- | src/fields/util.ts | 163 |
20 files changed, 213 insertions, 266 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 634b14822..a5190e66c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -7,7 +7,6 @@ import { Id } from '../../fields/FieldSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField, PointData } from '../../fields/InkField'; import { List } from '../../fields/List'; -import { ProxyField } from '../../fields/Proxy'; import { RichTextField } from '../../fields/RichTextField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; @@ -673,8 +672,6 @@ export namespace Docs { * haven't been initialized, the newly initialized prototype document. */ export async function initialize(): Promise<void> { - ProxyField.initPlugin(); - ComputedField.initPlugin(); // non-guid string ids for each document prototype const prototypeIds = Object.values(DocumentType) .filter(type => type !== DocumentType.NONE) @@ -799,7 +796,7 @@ export namespace Docs { const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_'); - dataProps['acl-Override'] = 'None'; + // dataProps['acl-Override'] = SharingPermissions.Unset; dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; dataProps.system = viewProps.system; @@ -825,7 +822,7 @@ export namespace Docs { const viewFirstProps: { [id: string]: any } = {}; viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - viewFirstProps['acl-Override'] = 'None'; + // viewFirstProps['acl-Override'] = SharingPermissions.Unset; viewFirstProps.author = Doc.CurrentUserEmail; let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default @@ -995,7 +992,7 @@ export namespace Docs { I.data = new InkField(points); I.creationDate = new DateField(); I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - I['acl-Override'] = 'None'; + //I['acl-Override'] = SharingPermissions.Unset; I.links = ComputedField.MakeFunction('links(self)'); I[Initializing] = false; return I; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 5549769aa..afa9e7de3 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -601,7 +601,6 @@ export class CurrentUserUtils { }; reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); reaction(() => UndoManager.undoStack.slice(), () => { - console.log(UndoManager.undoStack) Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4; }, { fireImmediately: true }); return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 588664dec..2b0ce1d3d 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -44,7 +44,6 @@ export class LinkManager { if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { Doc.GetProto(a1)[DirectLinksSym].add(link); Doc.GetProto(a2)[DirectLinksSym].add(link); - //Doc.GetProto(link)[DirectLinksSym].add(link); // bcz: links are not linked to themself, so this was a hack } }) ); @@ -147,12 +146,7 @@ export class LinkManager { return this.relatedLinker(anchor); } // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { - // FIXME:glr Why is Doc undefined? - if (Doc.GetProto(anchor)[DirectLinksSym]) { - return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); - } else { - return []; - } + return Array.from(Doc.GetProto(anchor)[DirectLinksSym] ?? []); } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 2d598c1ac..2d1f61cfb 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,6 +1,6 @@ -import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr"; -import { Field } from "../../fields/Doc"; -import { ClientUtils } from "./ClientUtils"; +import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr'; +import { Field } from '../../fields/Doc'; +import { ClientUtils } from './ClientUtils'; let serializing = 0; export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) { @@ -25,7 +25,7 @@ export namespace SerializationHelper { serializing++; if (!(obj.constructor.name in reverseMap)) { serializing--; - throw Error("Error: " + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); + throw Error('Error: ' + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); } const json = serialize(obj); @@ -59,24 +59,24 @@ export namespace SerializationHelper { const type = serializationTypes[obj.__type]; const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result))); if (type.afterDeserialize) { - await type.afterDeserialize(value); + type.afterDeserialize(value); } return value; } } -const serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; +const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; const reverseMap: { [ctor: string]: string } = {}; export interface DeserializableOpts { - (constructor: { new(...args: any[]): any }): void; + (constructor: { new (...args: any[]): any }): void; withFields(fields: string[]): Function; } export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts; -export function Deserializable(constructor: { new(...args: any[]): any }): void; -export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { - function addToMap(name: string, ctor: { new(...args: any[]): any }) { +export function Deserializable(constructor: { new (...args: any[]): any }): void; +export function Deserializable(constructor: { new (...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { + function addToMap(name: string, ctor: { new (...args: any[]): any }) { const schema = getDefaultModelSchema(ctor) as any; if (schema.targetClass !== ctor) { const newSchema = { ...schema, factory: () => new ctor() }; @@ -89,17 +89,20 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin throw new Error(`Name ${name} has already been registered as deserializable`); } } - if (typeof constructor === "string") { - return Object.assign((ctor: { new(...args: any[]): any }) => { - addToMap(constructor, ctor); - }, { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) }); + if (typeof constructor === 'string') { + return Object.assign( + (ctor: { new (...args: any[]): any }) => { + addToMap(constructor, ctor); + }, + { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) } + ); } addToMap(constructor.name, constructor); } export namespace Deserializable { export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) { - return function (constructor: { new(...fields: any[]): any }) { + return function (constructor: { new (...fields: any[]): any }) { Deserializable(name || constructor.name, afterDeserialize)(constructor); let schema = getDefaultModelSchema(constructor); if (schema) { @@ -128,7 +131,7 @@ export namespace Deserializable { factory: context => { const args = fields.map(key => context.json[key]); return new constructor(...args); - } + }, }; setDefaultModelSchema(constructor, schema); } @@ -138,7 +141,7 @@ export namespace Deserializable { export function autoObject(): PropSchema { return custom( - (s) => SerializationHelper.Serialize(s), + s => SerializationHelper.Serialize(s), (json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res)) ); -}
\ No newline at end of file +} diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index b7199f433..1289ca2b4 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,4 +1,4 @@ -@import "../views/global/globalCssVariables"; +@import '../views/global/globalCssVariables'; .settings-interface { //background-color: whitesmoke !important; @@ -59,7 +59,6 @@ } } - .password-content { display: flex; flex-direction: column; @@ -76,7 +75,6 @@ color: black; border-radius: 5px; padding: 7px; - } } @@ -148,7 +146,6 @@ margin-top: 2; text-align: left; } - } } @@ -297,9 +294,8 @@ margin-bottom: 10px; } - .error-text { - color: #C40233; + color: #c40233; width: 300; margin-left: -20; font-size: 10; @@ -313,7 +309,7 @@ font-size: 10; margin-bottom: 4; margin-top: -3; - color: #009F6B; + color: #009f6b; } .focus-span { @@ -352,7 +348,6 @@ padding: 0 0 0 20px; color: black; } - } } @@ -421,7 +416,6 @@ .tab-content { display: flex; - margin: 20px 0; .tab-column { flex: 0 0 50%; @@ -437,9 +431,7 @@ .tab-column-content { padding-left: 16px; } - } - } .tab-column button { @@ -465,4 +457,4 @@ .settings-interface .settings-heading { font-size: 25; } -}
\ No newline at end of file +} diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 5c1c836f7..a3d76591f 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -28,7 +28,7 @@ export enum ColorScheme { export enum freeformScrollMode { Pan = 'pan', - Zoom = 'zoom' + Zoom = 'zoom', } @observer @@ -307,11 +307,9 @@ export class SettingsManager extends React.Component<{}> { ); } - - setFreeformScrollMode = (mode: freeformScrollMode) => { Doc.UserDoc().freeformScrollMode = mode; - } + }; @computed get modesContent() { return ( @@ -334,12 +332,20 @@ export class SettingsManager extends React.Component<{}> { <div className="playground-text">Playground Mode</div> </div> </div> - <div className="tab-column-title">Freeform scroll mode</div> - <div> - <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}>Scroll to pan</button> - <div>Scrolling pans around the freeform, holding shift and scrolling zooms in and out.</div> - <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}>Scroll to zoom</button> - <div>Scrolling zooms in and out of canvas</div> + <div className="tab-column-title" style={{ marginTop: 10, marginBottom: 0 }}> + Freeform scrolling + </div> + <div className="tab-column-content"> + <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}> + Scroll to pan + </button> + <div> + <div>Scrolling pans canvas, shift + scrolling zooms</div> + </div> + <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}> + Scroll to zoom + </button> + <div>Scrolling zooms canvas</div> </div> </div> <div className="tab-column"> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 8b50a3b85..824a862cb 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -5,7 +5,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; -import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from '../../fields/Doc'; +import { AclAdmin, AclPrivate, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, HierarchyMapping, Opt } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; @@ -20,10 +21,9 @@ import { SearchBox } from '../views/search/SearchBox'; import { DocumentManager } from './DocumentManager'; import { GroupManager, UserOptions } from './GroupManager'; import { GroupMemberView } from './GroupMemberView'; +import { LinkManager } from './LinkManager'; import { SelectionManager } from './SelectionManager'; import './SharingManager.scss'; -import { LinkManager } from './LinkManager'; -import { Id } from '../../fields/FieldSymbols'; export interface User { email: string; @@ -82,16 +82,6 @@ export class SharingManager extends React.Component<{}> { @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not - // maps acl symbols to SharingPermissions - private AclMap = new Map<symbol, string>([ - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAugment, SharingPermissions.Augment], - [AclSelfEdit, SharingPermissions.SelfEdit], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin], - ]); - // private get linkVisible() { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; // } @@ -137,7 +127,6 @@ export class SharingManager extends React.Component<{}> { * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. */ populateUsers = async () => { - return; if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') { this.populating = true; const userList = await RequestPromise.get(Utils.prepend('/getUsers')); @@ -164,7 +153,7 @@ export class SharingManager extends React.Component<{}> { for (const sharer of sharingDocs) { if (!this.users.find(user => user.user.email === sharer.user.email)) { this.users.push(sharer); - // LinkManager.addLinkDB(sharer.linkDatabase); + //LinkManager.addLinkDB(sharer.linkDatabase); } } }); @@ -185,6 +174,7 @@ export class SharingManager extends React.Component<{}> { const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); return !docs + .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .map(doc => { doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); @@ -218,6 +208,7 @@ export class SharingManager extends React.Component<{}> { // ! ensures it returns true if document has been shared successfully, false otherwise return !docs + .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .map(doc => { doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); @@ -284,6 +275,7 @@ export class SharingManager extends React.Component<{}> { docs.forEach(doc => { const isDashboard = dashboards.indexOf(doc) !== -1; if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard); + this.setDashboardBackground(doc, permission as SharingPermissions); }); } }; @@ -291,14 +283,18 @@ export class SharingManager extends React.Component<{}> { /** * Sets the background of the Dashboard if it has been shared as a visual indicator */ - setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => { + setDashboardBackground = (doc: Doc, permission: SharingPermissions) => { if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) { if (permission !== SharingPermissions.None) { doc.isShared = true; doc.backgroundColor = 'green'; } else { const acls = doc[DataSym][AclSym]; - if (Object.keys(acls).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key])))) { + if ( + Object.keys(acls) + .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me') + .every(key => [AclUnset, AclPrivate].includes(acls[key])) + ) { doc.isShared = undefined; doc.backgroundColor = undefined; } @@ -373,7 +369,7 @@ export class SharingManager extends React.Component<{}> { private sharingOptions(uniform: boolean, override?: boolean) { const dropdownValues: string[] = Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift('-multiple-'); - if (override) dropdownValues.unshift('None'); + if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return dropdownValues .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any)) .map(permission => ( @@ -459,17 +455,6 @@ export class SharingManager extends React.Component<{}> { } }; - // distributeOverCollection = (targetDoc?: Doc) => { - // const target = targetDoc || this.targetDoc!; - - // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); - // docs.forEach(doc => { - // for (const [key, value] of Object.entries(doc[AclSym])) { - // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target); - // } - // }); - // } - /** * Sorting algorithm to sort users. */ @@ -492,6 +477,7 @@ export class SharingManager extends React.Component<{}> { * @returns the main interface of the SharingManager. */ @computed get sharingInterface() { + if (!this.targetDoc) return null; TraceMobx(); const groupList = GroupManager.Instance?.allGroups || []; const sortedUsers = this.users @@ -524,21 +510,21 @@ export class SharingManager extends React.Component<{}> { docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); } - const targetDoc = docs[0]; + const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DataSym]; // tslint:disable-next-line: no-unnecessary-callback-wrapper const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym])))); + const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).map(doc => doc?.[AclSym] && Object.keys(doc[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, 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 uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]); const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; return !permissions ? null : ( @@ -574,7 +560,7 @@ export class SharingManager extends React.Component<{}> { <div key={'me'} className={'container'}> <span className={'padding'}>Me</span> <div className="edit-actions"> - <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : '-multiple-'}</div> + <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div> </div> </div> ) : null @@ -585,7 +571,9 @@ export class SharingManager extends React.Component<{}> { groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" }); const groupListContents = groupListMap.map(group => { 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 uniform = docs + .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) + .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.title)}`]) : '-multiple-'; return !permissions ? null : ( diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index dc9c2eb6c..e87d2046b 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -29,6 +29,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select @observable private _items: Array<ContextMenuProps> = []; @observable private overItem = false; + @action componentDidMount() { this._items.length = 0; if ((this.props as SubmenuProps)?.subitems) { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 36ea3ef18..a6fa2f04b 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -79,7 +79,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1); getAnchor = () => { - console.log(document.activeElement); return this._subContentView?.getAnchor?.() || this.rootDoc; }; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 92c5708aa..8d495d286 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -7,7 +7,7 @@ import { intersection } from 'lodash'; import { action, autorun, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; +import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -364,7 +364,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @undoBatch changePermissions = (e: any, user: string) => { - const docs = SelectionManager.Views().length < 2 ? (this.selectedDoc ? [this.selectedDoc] : []) : SelectionManager.Views().map(docView => docView.props.Document); + const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc : DocCast(doc)[DataSym])); SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); }; @@ -374,7 +374,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { getPermissionsSelect(user: string, permission: string) { const dropdownValues: string[] = Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); - if (user === 'Override') dropdownValues.unshift('None'); + if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return ( <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> {dropdownValues @@ -449,16 +449,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { * @returns the sharing and permissions panel. */ @computed get sharingTable() { - const AclMap = new Map<symbol, string>([ - [AclUnset, 'None'], - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAugment, SharingPermissions.Augment], - [AclSelfEdit, SharingPermissions.SelfEdit], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin], - ]); - // all selected docs const docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); @@ -470,7 +460,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys: string[] = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym])))); + const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); const tableEntries = []; @@ -478,9 +468,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (commonKeys.length) { for (const key of commonKeys) { const name = denormalizeEmail(key.substring(4)); - const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key])); + const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key]); if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : '-multiple-')); + tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-')); } } } @@ -488,11 +478,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); - tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === docs[0]['acl-Public']) ? AclMap.get(target[AclSym]?.['acl-Public']) || SharingPermissions.None : '-multiple-')); + if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); + tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-')); tableEntries.unshift( - this.sharingItem('Me', showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : '-multiple-', !ownerSame) + this.sharingItem( + 'Me', + showAdmin, + docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-', + !ownerSame + ) ); - if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); return <div className="propertiesView-sharingTable">{tableEntries}</div>; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2c76a80a1..82b97dff0 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -806,9 +806,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }, }); const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type - if (intersects.length) { - console.log(); - } // return tuples of the inkingStroke intersected, and the t value of the intersection intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve } @@ -1022,7 +1019,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); e.preventDefault(); - switch (!e.ctrlKey ? freeformScrollMode.Pan : Doc.UserDoc().freeformScrollMode) { + switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { case freeformScrollMode.Pan: // if shift is selected then zoom if (e.ctrlKey) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 93dc0b2a1..cbe0a465d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -15,7 +15,7 @@ audiotag { position: absolute; cursor: pointer; border-radius: 10px; - width: 10px; + width: 12px; margin-top: -2px; font-size: 4px; background: lightblue; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 39328ae4a..b26f485be 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -280,7 +280,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame; if (activeFrame !== undefined) { const transTime = NumCast(activeItem.presTransition, 500); - const context = activeItem.presActiveFrame ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc); + const context = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc); if (context) { const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView; if (ffview) { @@ -1383,11 +1383,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {presEffect(PresEffect.Roll)} </div> </div> - <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}> + <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> <div className="presBox-subheading">Effect direction</div> <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div> </div> - <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}> + <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}> {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts index 8c8b83fbf..564829d54 100644 --- a/src/client/views/nodes/trails/PresEnums.ts +++ b/src/client/views/nodes/trails/PresEnums.ts @@ -23,6 +23,7 @@ export enum PresEffectDirection { Center = 'Enter from center', Top = 'Enter from Top', Bottom = 'Enter from bottom', + None = 'None', } export enum PresStatus { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 40152551e..6762665a2 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -113,28 +113,37 @@ export const Initializing = Symbol('Initializing'); export const ForceServerWrite = Symbol('ForceServerWrite'); export const CachedUpdates = Symbol('Cached updates'); -const AclMap = new Map<string, symbol>([ - ['None', AclUnset], - [SharingPermissions.None, AclPrivate], - [SharingPermissions.View, AclReadonly], - [SharingPermissions.Augment, AclAugment], - [SharingPermissions.SelfEdit, AclSelfEdit], - [SharingPermissions.Edit, AclEdit], - [SharingPermissions.Admin, AclAdmin], +export enum aclLevel { + unset = -1, + unshared = 0, + viewable = 1, + augmentable = 2, + selfEditable = 2.5, + editable = 3, + admin = 4, +} +// prettier-ignore +export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([ + [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }], + [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }], + [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}], + [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }], + [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }], + [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }], + [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }], ]); +export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }])); // 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; + const target = (doc as any)?.__fields ?? doc; + const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; + Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl)); + if (Object.keys(permissions).length || doc[AclSym]?.length) { + runInAction(() => (doc[AclSym] = permissions)); } if (doc.proto instanceof Promise) { @@ -267,7 +276,7 @@ export class Doc extends RefField { } constructor(id?: FieldId, forceSave?: boolean) { super(id); - const doc = new Proxy<this>(this, { + const docProxy = new Proxy<this>(this, { set: setter, get: getter, // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter @@ -296,11 +305,11 @@ export class Doc extends RefField { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); - this[SelfProxy] = doc; + this[SelfProxy] = docProxy; if (!id || forceSave) { - DocServer.CreateField(doc); + DocServer.CreateField(docProxy); } - return doc; + return docProxy; } proto: Opt<Doc>; @@ -329,6 +338,7 @@ export class Doc extends RefField { @observable private ___fields: any = {}; @observable private ___fieldKeys: any = {}; + /// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing @observable public [AclSym]: { [key: string]: symbol } = {}; @observable public [DirectLinksSym]: Set<Doc> = new Set(); @observable public [AnimationSym]: Opt<Doc>; @@ -939,6 +949,7 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = new Doc(copyProtoId, true); + updateCachedAcls(copy); const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []); Object.keys(doc).forEach(key => { if (exclude.includes(key)) return; @@ -989,6 +1000,7 @@ export namespace Doc { if (doc) { const delegate = new Doc(id, true); delegate[Initializing] = true; + updateCachedAcls(delegate); delegate.proto = doc; delegate.author = Doc.CurrentUserEmail; Object.keys(doc) diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index 8d040f493..e50c2856f 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -1,12 +1,12 @@ - -export const Update = Symbol("Update"); -export const Self = Symbol("Self"); -export const SelfProxy = Symbol("SelfProxy"); -export const HandleUpdate = Symbol("HandleUpdate"); -export const Id = Symbol("Id"); -export const OnUpdate = Symbol("OnUpdate"); -export const Parent = Symbol("Parent"); -export const Copy = Symbol("Copy"); -export const ToScriptString = Symbol("ToScriptString"); -export const ToPlainText = Symbol("ToPlainText"); -export const ToString = Symbol("ToString"); +export const Update = Symbol('Update'); +export const Self = Symbol('Self'); +export const SelfProxy = Symbol('SelfProxy'); +export const HandleUpdate = Symbol('HandleUpdate'); +export const Id = Symbol('Id'); +export const OnUpdate = Symbol('OnUpdate'); +export const Parent = Symbol('Parent'); +export const Copy = Symbol('Copy'); +export const ToValue = Symbol('ToValue'); +export const ToScriptString = Symbol('ToScriptString'); +export const ToPlainText = Symbol('ToPlainText'); +export const ToString = Symbol('ToString'); diff --git a/src/fields/List.ts b/src/fields/List.ts index 1e1adc7a8..9c7794813 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -127,6 +127,9 @@ const listHandlers: any = { this[Self].__realFields(); return this[Self].__fields.map(toRealField).join(separator); }, + lastElement() { + return this[Self].__realFields().lastElement(); + }, lastIndexOf(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); @@ -213,7 +216,7 @@ function toRealField(field: Field) { return field instanceof ProxyField ? field.value : field; } -function listGetter(target: any, prop: string | number | symbol, receiver: any): any { +function listGetter(target: any, prop: string | symbol, receiver: any): any { if (listHandlers.hasOwnProperty(prop)) { return listHandlers[prop]; } diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 1266e6e3c..55d1d9ea4 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -5,9 +5,8 @@ import { observable, action, runInAction, computed } from 'mobx'; import { DocServer } from '../client/DocServer'; import { RefField } from './RefField'; import { ObjectField } from './ObjectField'; -import { Id, Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Id, Copy, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; -import { Plugins } from './util'; function deserializeProxy(field: any) { if (!field.cache.field) { @@ -30,6 +29,10 @@ export class ProxyField<T extends RefField> extends ObjectField { } } + [ToValue](doc: any) { + return ProxyField.toValue(this); + } + [Copy]() { if (this.cache.field) return new ProxyField<T>(this.cache.field); return new ProxyField<T>(this.fieldId); @@ -105,12 +108,10 @@ export namespace ProxyField { } } - export function initPlugin() { - Plugins.addGetterPlugin((doc, _, value) => { - if (useProxy && value instanceof ProxyField) { - return { value: value.value }; - } - }); + export function toValue(value: any) { + if (useProxy) { + return { value: value.value }; + } } } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 4896c027d..b23732b45 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -6,11 +6,10 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba import { autoObject, Deserializable } from '../client/util/SerializationHelper'; import { numberRange } from '../Utils'; import { Doc, Field, Opt } from './Doc'; -import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; -import { Plugins } from './util'; function optional(propSchema: PropSchema) { return custom( @@ -175,6 +174,9 @@ export class ComputedField extends ScriptField { value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); + [ToValue](doc: Doc) { + return ComputedField.toValue(doc, this); + } [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } @@ -239,12 +241,10 @@ export namespace ComputedField { } } - export function initPlugin() { - Plugins.addGetterPlugin((doc, _, value) => { - if (useComputed && value instanceof ComputedField) { - return { value: value._valueOutsideReaction(doc), shouldReturn: true }; - } - }); + export function toValue(doc: any, value: any) { + if (useComputed) { + return { value: value._valueOutsideReaction(doc) }; + } } } diff --git a/src/fields/util.ts b/src/fields/util.ts index c1976aada..dc0b41276 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,4 +1,4 @@ -import { action, observable, runInAction, trace } from 'mobx'; +import { $mobx, action, observable, runInAction, trace } from 'mobx'; import { computedFn } from 'mobx-utils'; import { DocServer } from '../client/DocServer'; import { CollectionViewType } from '../client/documents/DocumentTypes'; @@ -8,13 +8,11 @@ import { returnZero } from '../Utils'; import CursorField from './CursorField'; import { AclAdmin, - AclAugment, AclEdit, + aclLevel, AclPrivate, - AclReadonly, AclSelfEdit, AclSym, - AclUnset, DataSym, Doc, DocListCast, @@ -22,13 +20,15 @@ import { FieldResult, ForceServerWrite, HeightSym, + HierarchyMapping, Initializing, LayoutSym, + ReverseHierarchyMap, updateCachedAcls, UpdatingFromServer, WidthSym, } from './Doc'; -import { Id, OnUpdate, Parent, Self, SelfProxy, Update } from './FieldSymbols'; +import { Id, OnUpdate, Parent, SelfProxy, ToValue, Update } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; @@ -47,19 +47,6 @@ export function TraceMobx() { tracing && trace(); } -export interface GetterResult { - value: FieldResult; - shouldReturn?: boolean; -} -export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined; -const getterPlugins: GetterPlugin[] = []; - -export namespace Plugins { - export function addGetterPlugin(plugin: GetterPlugin) { - getterPlugins.push(plugin); - } -} - const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { if (SerializationHelper.IsSerializing()) { target[prop] = value; @@ -118,6 +105,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (writeToServer) { if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } }); + if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } @@ -125,7 +113,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ redo: () => (receiver[prop] = value), - undo: () => (receiver[prop] = curValue), + undo: () => { + const wasUpdate = receiver[UpdatingFromServer]; + receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable. + receiver[prop] = curValue; + receiver[UpdatingFromServer] = wasUpdate; + }, prop: prop?.toString(), }); return true; @@ -182,8 +175,11 @@ export function inheritParentAcls(parent: Doc, child: Doc) { * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. * * None: the document is not shared with that user. + * + * Unset: Remove a sharing permission (eg., used ) */ export enum SharingPermissions { + Unset = 'None', Admin = 'Admin', Edit = 'Edit', SelfEdit = 'Self Edit', @@ -203,22 +199,16 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) { export function GetEffectiveAcl(target: any, user?: string): symbol { if (!target) return AclPrivate; if (target[UpdatingFromServer]) return AclAdmin; - // authored documents are private until an ACL is set. - if (!target[AclSym] && target.author && target.author !== Doc.CurrentUserEmail) return AclPrivate; return 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 || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent + if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable return GetEffectiveAcl(target); } -let HierarchyMapping: Map<symbol, number> | undefined; - let cachedGroups = observable([] as string[]); -/// 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 cachedGroups.includes(name); }, true); @@ -230,42 +220,32 @@ export function SetCachedGroups(groups: string[]) { } function getEffectiveAcl(target: any, user?: string): symbol { 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 - const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author - if (userChecked === targetAuthor || !targetAuthor) return AclAdmin; - if (GetCachedGroupByName('Admin')) return AclAdmin; + if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; + 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 (targetAcls && Object.keys(targetAcls).length) { - HierarchyMapping = - HierarchyMapping || - new Map<symbol, number>([ - [AclPrivate, 0], - [AclReadonly, 1], - [AclAugment, 2], - [AclSelfEdit, 2.5], - [AclEdit, 3], - [AclAdmin, 4], - ]); - 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 (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { - if (GetCachedGroupByName(entity) || userChecked === entity) { + if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) { + if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') { effectiveAcl = value as symbol; } } } // 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 = targetAcls['acl-Override']; - if (override !== AclUnset && override !== undefined) effectiveAcl = 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; + return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; } + // authored documents are private until an ACL is set. + const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author + if (targetAuthor && targetAuthor !== userChecked) return AclPrivate; return AclAdmin; } /** @@ -290,21 +270,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } visited.push(target); - const HierarchyMapping = new Map<string, number>([ - ['Not Shared', 0], - ['Can View', 1], - ['Can Augment', 2], - ['Self Edit', 2.5], - ['Can Edit', 3], - ['Admin', 4], - ]); - let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) - let dataDocChanged = false; - const dataDoc = target[DataSym]; - // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive - if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) { + if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) { target[key] = acl; layoutDocChanged = true; @@ -315,15 +283,16 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } } - if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { + let dataDocChanged = false; + const dataDoc = target[DataSym]; + if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) { if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; dataDocChanged = true; } // maps over the links of the document - const links = DocListCast(dataDoc.links); - links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); + DocListCast(dataDoc.links).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => { @@ -352,11 +321,10 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop); - + const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't - if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, 'None'].includes(value))) return true; + if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), 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('_')) { @@ -372,53 +340,44 @@ export function setter(target: any, in_prop: string | symbol | number, value: an return _setter(target, prop, value, receiver); } -export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { - let prop = in_prop; - - 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 (((typeof in_prop !== 'symbol' && in_prop !== 'constructor') || prop === HeightSym || prop === WidthSym) && 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('_')) { - if (!prop.startsWith('__')) prop = prop.substring(1); - if (target.__LAYOUT__) return target.__LAYOUT__[prop]; - } - if (prop === 'then') { - //If we're being awaited - return undefined; +export function getter(target: any, prop: string | symbol, proxy: any): any { + // prettier-ignore + switch (prop) { + case 'then' : return undefined; + case '__fields' : case '__id': + case 'constructor': case 'toString': case 'valueOf': + case 'factory': case 'serializeInfo': + return target[prop]; + case AclSym : return target[AclSym]; + case $mobx: return target.__fields[prop]; + case LayoutSym: return target.__Layout__; + case HeightSym: case WidthSym: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; + default : + if (typeof prop === 'symbol') return target[prop]; + if (prop.startsWith('isMobX')) return target[prop]; + if (prop.startsWith('__')) return target[prop]; + if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined; } - if (typeof prop === 'symbol') { - return target.__fields[prop] || target[prop]; - } - if (SerializationHelper.IsSerializing()) { - return target[prop]; - } - return getFieldImpl(target, prop, receiver); + + const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined; + if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop]; + return getFieldImpl(target, layout_prop ?? prop, proxy); } -function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { - receiver = receiver || target[SelfProxy]; - let field = target.__fields[prop]; - for (const plugin of getterPlugins) { - const res = plugin(receiver, prop, field); - if (res === undefined) continue; - if (res.shouldReturn) { - return res.value; - } else { - field = res.value; - } - } +function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { + const field = target.__fields[prop]; + const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys + if (value) return value.value; if (field === undefined && !ignoreProto && prop !== 'proto') { - const proto = getFieldImpl(target, 'proto', receiver, true); //TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters + const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { - return getFieldImpl(proto[Self], prop, receiver, ignoreProto); + return getFieldImpl(proto, prop, proxy, ignoreProto); } - return undefined; } return field; } export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { - return getFieldImpl(target, prop, undefined, ignoreProto); + return getFieldImpl(target, prop, target[SelfProxy], ignoreProto); } export function deleteProperty(target: any, prop: string | number | symbol) { |