diff options
Diffstat (limited to 'src/fields/util.ts')
-rw-r--r-- | src/fields/util.ts | 304 |
1 files changed, 171 insertions, 133 deletions
diff --git a/src/fields/util.ts b/src/fields/util.ts index 8fb35981b..cbb560114 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,20 +1,41 @@ -import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAugment, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing, AclSelfEdit } from "./Doc"; -import { SerializationHelper } from "../client/util/SerializationHelper"; -import { ProxyField, PrefetchProxy } from "./Proxy"; -import { RefField } from "./RefField"; -import { ObjectField } from "./ObjectField"; -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 } from "./Types"; -import { returnZero } from "../Utils"; -import CursorField from "./CursorField"; -import { List } from "./List"; -import { SnappingManager } from "../client/util/SnappingManager"; -import { computedFn } from "mobx-utils"; -import { RichTextField } from "./RichTextField"; +import { UndoManager } from '../client/util/UndoManager'; +import { + Doc, + FieldResult, + UpdatingFromServer, + LayoutSym, + AclPrivate, + AclEdit, + AclReadonly, + AclAugment, + AclSym, + DataSym, + DocListCast, + AclAdmin, + HeightSym, + WidthSym, + updateCachedAcls, + AclUnset, + DocListCastAsync, + ForceServerWrite, + Initializing, + AclSelfEdit, +} from './Doc'; +import { SerializationHelper } from '../client/util/SerializationHelper'; +import { ProxyField, PrefetchProxy } from './Proxy'; +import { RefField } from './RefField'; +import { ObjectField } from './ObjectField'; +import { action, observable, runInAction, trace } from 'mobx'; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from './FieldSymbols'; +import { DocServer } from '../client/DocServer'; +import { ComputedField } from './ScriptField'; +import { ScriptCast, StrCast } from './Types'; +import { returnZero } from '../Utils'; +import CursorField from './CursorField'; +import { List } from './List'; +import { SnappingManager } from '../client/util/SnappingManager'; +import { computedFn } from 'mobx-utils'; +import { RichTextField } from './RichTextField'; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -44,7 +65,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number return true; } - if (typeof prop === "symbol") { + if (typeof prop === 'symbol') { target[prop] = value; return true; } @@ -76,15 +97,13 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); - const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = - (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && (value instanceof RichTextField))) && - !DocServer.Control.isReadOnly(); + const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail; + const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly; + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && value instanceof RichTextField)) && !DocServer.Control.isReadOnly(); if (writeToDoc) { if (value === undefined) { - target.__fieldKeys && (delete target.__fieldKeys[prop]); + target.__fieldKeys && delete target.__fieldKeys[prop]; delete target.__fields[prop]; } else { target.__fieldKeys && (target.__fieldKeys[prop] = true); @@ -96,16 +115,17 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; 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 (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); + else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } }); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && + !receiver[Initializing] && + (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ - redo: () => receiver[prop] = value, - undo: () => receiver[prop] = curValue, - prop: prop?.toString() + redo: () => (receiver[prop] = value), + undo: () => (receiver[prop] = curValue), + prop: prop?.toString(), }); return true; } @@ -136,7 +156,6 @@ export function denormalizeEmail(email: string) { // playgroundMode = !playgroundMode; // } - /** * Copies parent's acl fields to the child */ @@ -145,35 +164,37 @@ export function inheritParentAcls(parent: Doc, child: Doc) { const dataDoc = parent[DataSym]; for (const key of Object.keys(dataDoc)) { // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. - const permission = (key === "acl-Public" && Doc.defaultAclPrivate) ? AclPrivate : dataDoc[key]; - key.startsWith("acl") && distributeAcls(key, permission, child); + const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key]; + key.startsWith('acl') && distributeAcls(key, permission, child); } } /** * These are the various levels of access a user can have to a document. - * + * * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. - * + * * 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 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. - * + * * None: the document is not shared with that user. */ export enum SharingPermissions { - Admin = "Admin", - Edit = "Edit", - SelfEdit = "Self Edit", - Augment = "Augment", - View = "View", - None = "Not Shared" + Admin = 'Admin', + Edit = 'Edit', + SelfEdit = 'Self Edit', + Augment = 'Augment', + View = 'View', + None = 'Not Shared', } // return acl from cache or cache the acl and return. -const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, 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. @@ -183,33 +204,47 @@ export function GetEffectiveAcl(target: any, user?: string): symbol { 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) + 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 (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent if (prop && DocServer.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); +export function GetCachedGroupByName(name: string) { + return getCachedGroupByNameCache(name); +} +export function SetCachedGroups(groups: string[]) { + runInAction(() => cachedGroups.push(...groups)); +} 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 + 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 (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; + if (GetCachedGroupByName('Admin')) return AclAdmin; 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] - ]); + 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)) { @@ -217,14 +252,14 @@ function getEffectiveAcl(target: any, user?: string): symbol { // 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 (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { + if (GetCachedGroupByName(entity) || userChecked === entity) { 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"]; + 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) @@ -246,12 +281,12 @@ 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] + ['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]) @@ -271,7 +306,6 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { - if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; dataDocChanged = true; @@ -282,7 +316,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc 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 => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } const data = d[DataSym]; @@ -292,7 +326,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc }); // maps over the annotations of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } const data = d[DataSym]; @@ -311,11 +345,11 @@ export function setter(target: any, in_prop: string | symbol | number, value: an const effectiveAcl = 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, 'None'].includes(value))) return true; // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true; - if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && prop.startsWith("_")) { - if (!prop.startsWith("__")) prop = prop.substring(1); + if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { + if (!prop.startsWith('__')) prop = prop.substring(1); if (target.__LAYOUT__) { target.__LAYOUT__[prop] = value; return true; @@ -331,17 +365,18 @@ export function getter(target: any, in_prop: string | symbol | number, receiver: 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 (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) 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 (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 + if (prop === 'then') { + //If we're being awaited return undefined; } - if (typeof prop === "symbol") { + if (typeof prop === 'symbol') { return target.__fields[prop] || target[prop]; } if (SerializationHelper.IsSerializing()) { @@ -362,22 +397,21 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP field = res.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 + 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 if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { return getFieldImpl(proto[Self], prop, receiver, ignoreProto); } return undefined; } return field; - } export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { return getFieldImpl(target, prop, undefined, ignoreProto); } export function deleteProperty(target: any, prop: string | number | symbol) { - if (typeof prop === "symbol") { + if (typeof prop === 'symbol') { delete target[prop]; return true; } @@ -389,64 +423,68 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any let lastValue = ObjectField.MakeCopy(value); return (diff?: any) => { const op = - diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } : - diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } - : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; + diff?.op === '$addToSet' + ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + : diff?.op === '$remFromSet' + ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } }; !op.$set && ((op as any).length = diff.length); const prevValue = ObjectField.MakeCopy(lastValue as List<any>); lastValue = ObjectField.MakeCopy(value); const newValue = ObjectField.MakeCopy(value); - if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { - !receiver[UpdatingFromServer] && UndoManager.AddEvent( - diff?.op === "$addToSet" ? - { - redo: () => { - receiver[prop].push(...diff.items.map((item: any) => item.value ? item.value() : item)); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: action(() => { - // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - prop: "" - } : - diff?.op === "$remFromSet" ? - { - redo: action(() => { - diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - undo: () => { - // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: "" - } + if (!(value instanceof CursorField) && !value?.some?.((v: any) => v instanceof CursorField)) { + !receiver[UpdatingFromServer] && + UndoManager.AddEvent( + diff?.op === '$addToSet' + ? { + redo: () => { + receiver[prop].push(...diff.items.map((item: any) => (item.value ? item.value() : item))); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + undo: action(() => { + // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo + diff.items.forEach((item: any) => { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }), + prop: '', + } + : diff?.op === '$remFromSet' + ? { + redo: action(() => { + diff.items.forEach((item: any) => { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }), + undo: () => { + // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo + diff.items.forEach((item: any) => { + const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); + }); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + prop: '', + } : { - redo: () => { - receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: () => { - // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo - receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: "" - }); + redo: () => { + receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + undo: () => { + // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo + receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + prop: '', + } + ); } target[Update](op); }; -}
\ No newline at end of file +} |