aboutsummaryrefslogtreecommitdiff
path: root/src/fields/util.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields/util.ts')
-rw-r--r--src/fields/util.ts237
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]);