aboutsummaryrefslogtreecommitdiff
path: root/src/fields
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields')
-rw-r--r--src/fields/Doc.ts75
-rw-r--r--src/fields/List.ts5
-rw-r--r--src/fields/util.ts101
3 files changed, 89 insertions, 92 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 54d85ba86..c8d28b4a2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -25,7 +25,6 @@ import JSZip = require("jszip");
import { saveAs } from "file-saver";
import { CollectionDockingView } from "../client/views/collections/CollectionDockingView";
import { SelectionManager } from "../client/util/SelectionManager";
-import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -103,22 +102,28 @@ const AclMap = new Map<string, symbol>([
[SharingPermissions.Admin, AclAdmin]
]);
-export function fetchProto(doc: Doc) {
+// caches the document access permissions for the current user.
+// this recursively updates all protos as well.
+export function updateCachedAcls(doc: Doc) {
if (!doc) return;
const permissions: { [key: string]: symbol } = {};
+ doc[UpdatingFromServer] = true;
Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
+ doc[UpdatingFromServer] = false;
- if (Object.keys(permissions).length) doc[AclSym] = permissions;
+ if (Object.keys(permissions).length) {
+ doc[AclSym] = permissions;
+ }
if (doc.proto instanceof Promise) {
- doc.proto.then(fetchProto);
+ doc.proto.then(updateCachedAcls);
return doc.proto;
}
}
@scriptingGlobal
-@Deserializable("Doc", fetchProto).withFields(["id"])
+@Deserializable("Doc", updateCachedAcls).withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
@@ -234,18 +239,16 @@ export class Doc extends RefField {
const prev = GetEffectiveAcl(this);
this[UpdatingFromServer] = true;
this[fKey] = value;
+ this[UpdatingFromServer] = false;
if (fKey.startsWith("acl")) {
- fetchProto(this);
+ updateCachedAcls(this);
}
- this[UpdatingFromServer] = false;
if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
DocServer.GetRefField(this[Id], true);
}
- if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) {
- this[UpdatingFromServer] = true;
- this[FieldsSym](true);
- this[UpdatingFromServer] = false;
- }
+ // if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) {
+ // this[FieldsSym](true);
+ // }
};
if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
@@ -676,16 +679,15 @@ export namespace Doc {
} else {
templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
if (!targetDoc[expandedLayoutFieldKey]) {
- const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
- // the template's arguments are stored in params which is derefenced to find
- // the actual field key where the parameterized template data is stored.
- newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
- newLayoutDoc.rootDocument = targetDoc;
- const dataDoc = Doc.GetProto(targetDoc);
- newLayoutDoc.resolvedDataDoc = dataDoc;
-
_pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true);
setTimeout(() => {
+ const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
+ // the template's arguments are stored in params which is derefenced to find
+ // the actual field key where the parameterized template data is stored.
+ newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
+ newLayoutDoc.rootDocument = targetDoc;
+ const dataDoc = Doc.GetProto(targetDoc);
+ newLayoutDoc.resolvedDataDoc = dataDoc;
if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) {
dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
}
@@ -886,17 +888,17 @@ export namespace Doc {
export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
export function UserDoc(): Doc { return manager._user_doc; }
export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); }
+ export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); }
export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; }
export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; }
export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); }
- export function IsSearchMatch(doc: Doc) {
- return computedFn(function IsSearchMatch(doc: Doc) {
- return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
- brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
- })(doc);
- }
+ const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) {
+ return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
+ brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
+ });
+ export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); }
export function IsSearchMatchUnmemoized(doc: Doc) {
return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
@@ -918,11 +920,9 @@ export namespace Doc {
brushManager.SearchMatchDoc.clear();
}
- export function IsBrushed(doc: Doc) {
- return computedFn(function IsBrushed(doc: Doc) {
- return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc));
- })(doc);
- }
+ const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); });
+ export function IsBrushed(doc: Doc) { return isBrushedCache(doc); }
+
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
@@ -1053,10 +1053,11 @@ export namespace Doc {
const container = target ?? CollectionDockingView.Instance.props.Document;
const docFilters = Cast(container._docFilters, listSpec("string"), []);
runInAction(() => {
- for (let i = 0; i < docFilters.length; i += 3) {
- if (docFilters[i] === key && (docFilters[i + 1] === value || modifiers === "match" || modifiers === "remove")) {
- if (docFilters[i + 2] === modifiers && modifiers && docFilters[i + 1] === value) return;
- docFilters.splice(i, 3);
+ for (let i = 0; i < docFilters.length; i++) {
+ const fields = docFilters[i].split(":"); // split key:value:modifier
+ if (fields[0] === key && (fields[1] === value || modifiers === "match" || modifiers === "remove")) {
+ if (fields[2] === modifiers && modifiers && fields[1] === value) return;
+ docFilters.splice(i, 1);
container._docFilters = new List<string>(docFilters);
break;
}
@@ -1065,9 +1066,7 @@ export namespace Doc {
if (!docFilters.length && modifiers === "match" && value === undefined) {
container._docFilters = undefined;
} else if (modifiers !== "remove") {
- docFilters.push(key);
- docFilters.push(value);
- docFilters.push(modifiers);
+ docFilters.push(key + ":" + value + ":" + modifiers);
container._docFilters = new List<string>(docFilters);
}
}
diff --git a/src/fields/List.ts b/src/fields/List.ts
index ceb538b2d..a0cbebaf5 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -43,7 +43,7 @@ const listHandlers: any = {
}
}
const res = list.__fields.push(...items);
- this[Update]({ op: "$addToSet", items });
+ this[Update]({ op: "$addToSet", items, length: length + items.length });
return res;
}),
reverse() {
@@ -77,7 +77,8 @@ const listHandlers: any = {
}
}
const res = list.__fields.splice(start, deleteCount, ...items);
- this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed } : undefined);
+ this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } :
+ items.length && !deleteCount ? { op: "$addToSet", items, length: list.__fields.length } : undefined);
return res.map(toRealField);
}),
unshift(...items: any[]) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 7293db0c2..d48011194 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,17 +1,19 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
-import { action, trace } from "mobx";
-import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols";
+import { action, trace, } from "mobx";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
import { ComputedField } from "./ScriptField";
import { ScriptCast, StrCast } 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";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -22,6 +24,7 @@ export function TraceMobx() {
tracing && trace();
}
+
export interface GetterResult {
value: FieldResult;
shouldReturn?: boolean;
@@ -111,10 +114,6 @@ export function makeReadOnly() {
export function makeEditable() {
_setter = _setterImpl;
}
-var _overrideAcl = false;
-export function OVERRIDE_acl(val: boolean) {
- _overrideAcl = val;
-}
export function normalizeEmail(email: string) {
return email.replace(/\./g, '__');
@@ -130,14 +129,6 @@ export function denormalizeEmail(email: string) {
// playgroundMode = !playgroundMode;
// }
-// the list of groups that the current user is a member of
-let currentUserGroups: string[] = [];
-
-// called from GroupManager once the groups have been fetched from the server
-export function setGroups(groups: string[]) {
- currentUserGroups = groups;
-}
-
/**
* These are the various levels of access a user can have to a document.
*
@@ -145,7 +136,7 @@ export function setGroups(groups: string[]) {
*
* Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document.
*
- * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything.
+ * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything.
*
* View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
*
@@ -154,33 +145,38 @@ export function setGroups(groups: string[]) {
export enum SharingPermissions {
Admin = "Admin",
Edit = "Can Edit",
- Add = "Can Add",
+ Add = "Can Augment",
View = "Can 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);
+
/**
* Calculates the effective access right to a document for the current user.
*/
-export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
- if (!target) return AclPrivate;
-
- // all changes received fromt the server must be processed as Admin
- if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
-
- // if the current user is the author of the document / the current user is a member of the admin group
- const userChecked = user || Doc.CurrentUserEmail;
- if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
- if (currentUserGroups.includes("Admin")) return AclAdmin;
+export function GetEffectiveAcl(target: any, user?: string): symbol {
+ return !target ? AclPrivate :
+ target[UpdatingFromServer] ? AclAdmin : getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
+}
+function getPropAcl(target: any, prop: string | symbol | number) {
+ if (prop === UpdatingFromServer || target[UpdatingFromServer] || prop == AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
+ if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable
+ return GetEffectiveAcl(target);
+}
- if (target[AclSym] && Object.keys(target[AclSym]).length) {
+let HierarchyMapping: Map<symbol, number> | undefined;
- // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified)
- if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
+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
+ if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author
+ if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin;
- let effectiveAcl = AclPrivate;
- const HierarchyMapping = new Map<symbol, number>([
+ if (targetAcls && Object.keys(targetAcls).length) {
+ HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
[AclPrivate, 0],
[AclReadonly, 1],
[AclAddonly, 2],
@@ -188,21 +184,21 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
[AclAdmin, 4]
]);
- for (const [key, value] of Object.entries(target[AclSym])) {
+ let effectiveAcl = AclPrivate;
+ for (const [key, value] of Object.entries(targetAcls)) {
// there are issues with storing fields with . in the name, so they are replaced with _ during creation
// as a result we need to restore them again during this comparison.
const entity = denormalizeEmail(key.substring(4)); // an individual or a group
- if (currentUserGroups.includes(entity) || userChecked === entity) {
- if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+ if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+ if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
effectiveAcl = value as symbol;
- if (effectiveAcl === AclAdmin) return effectiveAcl;
}
}
}
// if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
- const override = target[AclSym]["acl-Override"];
- if (override !== AclUnset && override !== undefined) effectiveAcl = target[AclSym]["acl-Override"];
+ const override = targetAcls["acl-Override"];
+ if (override !== AclUnset && override !== undefined) effectiveAcl = override;
// if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl;
@@ -225,7 +221,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
const HierarchyMapping = new Map<string, number>([
["Not Shared", 0],
["Can View", 1],
- ["Can Add", 2],
+ ["Can Augment", 2],
["Can Edit", 3],
["Admin", 4]
]);
@@ -274,20 +270,20 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
});
}
- layoutDocChanged && fetchProto(target); // updates target[AclSym] when changes to acls have been made
- dataDocChanged && fetchProto(dataDoc);
+ layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made
+ dataDocChanged && updateCachedAcls(dataDoc);
}
const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
"chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- const effectiveAcl = GetEffectiveAcl(target, in_prop);
+ const effectiveAcl = getPropAcl(target, prop);
if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
// if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true;
- // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true;
+ // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
@@ -308,12 +304,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
let prop = in_prop;
- if (in_prop === "toString" || in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop];
- if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
- if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
- if (prop === LayoutSym) {
- return target.__LAYOUT__;
- }
+ 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("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
@@ -371,9 +365,12 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
export function updateFunction(target: any, prop: any, value: any, receiver: any) {
let current = ObjectField.MakeCopy(value);
return (diff?: any) => {
- const op = diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } :
- diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
- : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ 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) } };
+ !op.$set && ((op as any).length = diff.length);
+
const oldValue = current;
const newValue = ObjectField.MakeCopy(value);
current = newValue;