diff options
Diffstat (limited to 'src/fields/util.ts')
-rw-r--r-- | src/fields/util.ts | 237 |
1 files changed, 95 insertions, 142 deletions
diff --git a/src/fields/util.ts b/src/fields/util.ts index d87bb6656..70d9ed61f 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,33 +1,34 @@ -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'; +import { LinkManager } from '../client/util/LinkManager'; import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; import { returnZero } from '../Utils'; import CursorField from './CursorField'; import { AclAdmin, - AclAugment, AclEdit, + aclLevel, AclPrivate, - AclReadonly, AclSelfEdit, AclSym, - AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, - 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'; @@ -46,29 +47,12 @@ 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()) { + if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') { target[prop] = value; return true; } - if (typeof prop === 'symbol') { - target[prop] = value; - return true; - } if (value !== undefined) { value = value[SelfProxy] || value; } @@ -107,16 +91,13 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number delete target.__fields[prop]; } else { target.__fieldKeys && (target.__fieldKeys[prop] = true); - // if (target.__fields[prop] !== value) { - // console.log("ASSIGN " + prop + " " + value); - // } target.__fields[prop] = value; } - //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 (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } @@ -124,7 +105,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; @@ -149,13 +135,6 @@ export function denormalizeEmail(email: string) { return email.replace(/__/g, '.'); } -// playground mode allows the user to add/delete documents or make layout changes without them saving to the server -// let playgroundMode = false; - -// export function togglePlaygroundMode() { -// playgroundMode = !playgroundMode; -// } - /** * Copies parent's acl fields to the child */ @@ -181,8 +160,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', @@ -202,22 +184,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); @@ -229,42 +205,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; } /** @@ -277,24 +243,21 @@ function getEffectiveAcl(target: any, user?: string): symbol { */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) { if (!visited) visited = [] as Doc[]; - if (visited.includes(target)) return; - visited.push(target); + if (!target || visited.includes(target)) return; - const HierarchyMapping = new Map<string, number>([ - ['Not Shared', 0], - ['Can View', 1], - ['Can Augment', 2], - ['Self Edit', 2.5], - ['Can Edit', 3], - ['Admin', 4], - ]); + if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { + target[key] = acl; + if (target !== Doc.GetProto(target)) { + //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???) + updateCachedAcls(target); + } + return; + } + visited.push(target); 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; @@ -305,34 +268,27 @@ 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)); + LinkManager.Links(dataDoc).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)]).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - // } - const data = d[DataSym]; - if (data) { - distributeAcls(key, acl, data, inheritingFromCollection, visited); - } + distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited); }); // maps over the annotations of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - // } - const data = d[DataSym]; - if (data) { - distributeAcls(key, acl, data, inheritingFromCollection, visited); - } + distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited); }); } @@ -342,11 +298,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 = 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") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].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 !== '__id' && prop !== '__fields' && prop.startsWith('_')) { if (!prop.startsWith('__')) prop = prop.substring(1); @@ -355,67 +310,60 @@ export function setter(target: any, in_prop: string | symbol | number, value: an return true; } } - if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { - return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + if (target.__fields[prop] instanceof ComputedField) { + if (target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { + return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + } } 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 (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) { if (typeof prop === 'symbol') { delete target[prop]; - return true; + } else { + target[SelfProxy][prop] = undefined; } - target[SelfProxy][prop] = undefined; return true; } @@ -439,14 +387,19 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === '$addToSet' ? { redo: () => { - receiver[prop].push(...diff.items.map((item: any) => (item.value ? item.value() : item))); + receiver[prop].push(...diff.items.map((item: any) => 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); + if (item instanceof SchemaHeaderField) { + const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && receiver[prop].splice(ind, 1); + } else { + const ind = receiver[prop].indexOf(item.value ?? item); + ind !== -1 && receiver[prop].splice(ind, 1); + } }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), @@ -456,7 +409,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any ? { redo: action(() => { diff.items.forEach((item: any) => { - const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ? item.value() : item); + const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item); ind !== -1 && receiver[prop].splice(ind, 1); }); lastValue = ObjectField.MakeCopy(receiver[prop]); @@ -468,8 +421,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item); } else { - 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); + const ind = (prevValue as List<any>).indexOf(item.value ?? item); + ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item); } }); lastValue = ObjectField.MakeCopy(receiver[prop]); |