aboutsummaryrefslogtreecommitdiff
path: root/src/fields
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields')
-rw-r--r--src/fields/Doc.ts173
-rw-r--r--src/fields/List.ts8
-rw-r--r--src/fields/ScriptField.ts2
-rw-r--r--src/fields/documentSchemas.ts3
-rw-r--r--src/fields/util.ts198
5 files changed, 218 insertions, 166 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 470f9d4be..4c3c45d92 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -19,42 +19,30 @@ import { DateField } from "./DateField";
import { listSpec } from "./Schema";
import { ComputedField, ScriptField } from "./ScriptField";
import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
-import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions } from "./util";
+import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions, normalizeEmail } from "./util";
import { LinkManager } from "../client/util/LinkManager";
import JSZip = require("jszip");
import { saveAs } from "file-saver";
import { CollectionDockingView } from "../client/views/collections/CollectionDockingView";
import { SelectionManager } from "../client/util/SelectionManager";
+import { DocumentView } from "../client/views/nodes/DocumentView";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
const onDelegate = Object.keys(doc).includes(key);
-
const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- if (Field.IsField(field)) {
- return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
- }
- return "";
+ return !Field.IsField(field) ? "" : (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
}
export function toScriptString(field: Field): string {
- if (typeof field === "string") {
- return `"${field}"`;
- } else if (typeof field === "number" || typeof field === "boolean") {
- return String(field);
- } else {
- return field[ToScriptString]();
- }
+ if (typeof field === "string") return `"${field}"`;
+ if (typeof field === "number" || typeof field === "boolean") return String(field);
+ return field[ToScriptString]();
}
export function toString(field: Field): string {
- if (typeof field === "string") {
- return field;
- } else if (typeof field === "number" || typeof field === "boolean") {
- return String(field);
- } else if (field instanceof ObjectField) {
- return field[ToString]();
- } else if (field instanceof RefField) {
- return field[ToString]();
- }
+ if (typeof field === "string") return field;
+ if (typeof field === "number" || typeof field === "boolean") return String(field);
+ if (field instanceof ObjectField) return field[ToString]();
+ if (field instanceof RefField) return field[ToString]();
return "";
}
export function IsField(field: any): field is Field;
@@ -86,16 +74,10 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
}
-export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> {
- return Cast(field, Doc);
-}
+export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); }
-export function DocListCast(field: FieldResult): Doc[] {
- return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
-}
-export function DocListCastOrNull(field: FieldResult) {
- return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined;
-}
+export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; }
+export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; }
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
@@ -103,15 +85,18 @@ export const DataSym = Symbol("Data");
export const LayoutSym = Symbol("Layout");
export const FieldsSym = Symbol("Fields");
export const AclSym = Symbol("Acl");
+export const AclUnset = Symbol("AclUnset");
export const AclPrivate = Symbol("AclOwnerOnly");
export const AclReadonly = Symbol("AclReadOnly");
export const AclAddonly = Symbol("AclAddonly");
export const AclEdit = Symbol("AclEdit");
export const AclAdmin = Symbol("AclAdmin");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
+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.Add, AclAddonly],
@@ -119,25 +104,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 } = {};
- Object.keys(doc).filter(key => key.startsWith("acl")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!);
-
- if (Object.keys(permissions).length) doc[AclSym] = permissions;
+ doc[UpdatingFromServer] = true;
+ Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
+ doc[UpdatingFromServer] = false;
- if (GetEffectiveAcl(doc) === AclPrivate) {
- runInAction(() => doc[FieldsSym](true));
+ 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);
@@ -191,27 +179,26 @@ export class Doc extends RefField {
}
}
private get __fieldKeys() { return this.___fieldKeys; }
- private set __fieldKeys(value) {
- this.___fieldKeys = value;
- }
-
- @observable
- private ___fields: any = {};
+ private set __fieldKeys(value) { this.___fieldKeys = value; }
- @observable
- private ___fieldKeys: any = {};
- @observable
- public [AclSym]: { [key: string]: symbol };
+ @observable private ___fields: any = {};
+ @observable private ___fieldKeys: any = {};
+ @observable public [AclSym]: { [key: string]: symbol };
private [UpdatingFromServer]: boolean = false;
+ private [ForceServerWrite]: boolean = false;
private [Update] = (diff: any) => {
- !this[UpdatingFromServer] && DocServer.UpdateField(this[Id], diff);
+ (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff);
}
private [Self] = this;
private [SelfProxy]: any;
- public [FieldsSym](clear?: boolean) { return clear ? this.___fields = this.___fieldKeys = {} : this.___fields; }
+ public [FieldsSym](clear?: boolean) {
+ const self = this[SelfProxy];
+ runInAction(() => clear && Array.from(Object.keys(self)).forEach(key => delete self[key]));
+ return this.___fields;
+ }
public [WidthSym] = () => NumCast(this[SelfProxy]._width);
public [HeightSym] = () => NumCast(this[SelfProxy]._height);
public [ToScriptString] = () => `DOC-"${this[Self][Id]}"-`;
@@ -240,6 +227,7 @@ export class Doc extends RefField {
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
public static CurrentUserEmail: string = "";
+ public static get CurrentUserEmailNormalized() { return normalizeEmail(Doc.CurrentUserEmail); }
public async [HandleUpdate](diff: any) {
const set = diff.$set;
const sameAuthor = this.author === Doc.CurrentUserEmail;
@@ -254,13 +242,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[FieldsSym](true);
+ // }
};
if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
@@ -332,7 +323,7 @@ export namespace Doc {
export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
try {
return getField(doc[Self], key, ignoreProto);
- } catch {
+ } catch {
return doc;
}
}
@@ -411,7 +402,7 @@ export namespace Doc {
// and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
export function GetProto(doc: Doc): Doc {
if (doc instanceof Promise) {
- console.log("GetProto: warning: got Promise insead of Doc");
+ // console.log("GetProto: warning: got Promise insead of Doc");
}
const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
return proto === doc ? proto : Doc.GetProto(proto);
@@ -512,7 +503,6 @@ export namespace Doc {
alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`);
}
alias.author = Doc.CurrentUserEmail;
- alias[AclSym] = doc[AclSym];
Doc.AddDocToList(doc[DataSym], "aliases", alias);
@@ -691,23 +681,22 @@ 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(() => {
+ setTimeout(action(() => {
+ 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 });
}
targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
_pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args);
- });
+ }));
}
}
}
@@ -883,6 +872,7 @@ export namespace Doc {
export class DocData {
@observable _user_doc: Doc = undefined!;
+ @observable _sharing_doc: Doc = undefined!;
@observable _searchQuery: string = "";
}
@@ -895,21 +885,31 @@ export namespace Doc {
export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; }
export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
export function LayoutFieldKey(doc: Doc): string { return StrCast(Doc.Layout(doc).layout).split("'")[1]; }
+ export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
+ return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
+ }
+ export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeWidth"], useWidth ? doc[WidthSym]() : 0)); }
+ export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { return !doc ? 0 : NumCast(doc._nativeHeight, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeHeight"], useHeight ? doc[HeightSym]() : 0)); }
+ export function SetNativeWidth(doc: Doc, width: number | undefined) { doc[Doc.LayoutFieldKey(doc) + "-nativeWidth"] = width; }
+ export function SetNativeHeight(doc: Doc, height: number | undefined) { doc[Doc.LayoutFieldKey(doc) + "-nativeHeight"] = height; }
+
+
const manager = new DocData();
export function SearchQuery(): string { return manager._searchQuery; }
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) { manager._user_doc = doc; }
+ 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;
@@ -931,11 +931,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;
@@ -1024,6 +1022,8 @@ export namespace Doc {
const fieldVal = doc[key];
if (Cast(fieldVal, listSpec("string"), []).length) {
const vals = Cast(fieldVal, listSpec("string"), []);
+ const docs = vals.some(v => (v as any) instanceof Doc);
+ if (docs) return value === Field.toString(fieldVal as Field);
return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
const fieldStr = Field.toString(fieldVal as Field);
@@ -1064,10 +1064,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;
}
@@ -1076,9 +1077,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);
}
}
@@ -1099,14 +1098,14 @@ export namespace Doc {
export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) {
runInAction(() => {
- if (layoutDoc._nativeWidth || layoutDoc._nativeHeight) {
+ if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) {
layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
layoutDoc._nativeWidth = undefined;
layoutDoc._nativeHeight = undefined;
}
else {
layoutDoc._autoHeight = false;
- if (!layoutDoc._nativeWidth) {
+ if (!Doc.NativeWidth(layoutDoc)) {
layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
}
diff --git a/src/fields/List.ts b/src/fields/List.ts
index c9e4bd3c1..215dff34b 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]();
+ this[Update]({ op: "$addToSet", items, length: length + items.length });
return res;
}),
reverse() {
@@ -66,6 +66,7 @@ const listHandlers: any = {
this[Self].__realFields(); // coerce retrieving entire array
items = items.map(toObjectField);
const list = this[Self];
+ const removed = list.__fields.filter((item: any, i: number) => i >= start && i < start + deleteCount);
for (let i = 0; i < items.length; i++) {
const item = items[i];
//TODO Error checking to make sure parent doesn't already exist
@@ -76,7 +77,8 @@ const listHandlers: any = {
}
}
const res = list.__fields.splice(start, deleteCount, ...items);
- this[Update]();
+ this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } :
+ items.length && !deleteCount && start === list.__fields.length ? { op: "$addToSet", items, length: list.__fields.length } : undefined);
return res.map(toRealField);
}),
unshift(...items: any[]) {
@@ -314,7 +316,7 @@ class ListImpl<T extends Field> extends ObjectField {
// console.log(diff);
const update = this[OnUpdate];
// update && update(diff);
- update?.();
+ update?.(diff);
}
private [Self] = this;
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 47efccc99..024017302 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -196,7 +196,7 @@ export class ComputedField extends ScriptField {
}
Scripting.addGlobal(function getIndexVal(list: any[], index: number) {
- return list.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any);
+ return list?.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any);
}, "returns the value at a given index of a list", "(list: any[], index: number)");
Scripting.addGlobal(function makeScript(script: string) {
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 71294c59c..e0404d9d3 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -15,11 +15,11 @@ export const documentSchema = createSchema({
// "Location" properties in a very general sense
_curPage: "number", // current page of a page based document
_currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide)
+ _fullScreenView: Doc, // alias to display when double-clicking to open document in a full-screen view
lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide)
activeFrame: "number", // the active frame of a frame based animated document
_currentTimecode: "number", // current play back time of a temporal document (video / audio)
displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
- inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently
isLabel: "boolean", // whether the document is a label or not (video / audio)
audioStart: "number", // the time frame where the audio should begin playing
audioEnd: "number", // the time frame where the audio should stop playing
@@ -34,6 +34,7 @@ export const documentSchema = createSchema({
_scrollLeft: "number", // scroll position of a scrollable document (pdf, text, web)
// appearance properties on the layout
+ "_backgroundGrid-spacing": "number", // the size of the grid for collection views
_autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
_nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
_nativeHeight: "number", // "
diff --git a/src/fields/util.ts b/src/fields/util.ts
index fe3eea69d..ecb3fb343 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,18 +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 } 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 "@material-ui/core";
-
+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");
@@ -23,6 +24,7 @@ export function TraceMobx() {
tracing && trace();
}
+
export interface GetterResult {
value: FieldResult;
shouldReturn?: boolean;
@@ -94,7 +96,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
} else {
DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
}
- UndoManager.AddEvent({
+ !receiver[UpdatingFromServer] && UndoManager.AddEvent({
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
});
@@ -112,9 +114,12 @@ 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, '__');
+}
+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
@@ -124,14 +129,6 @@ export function OVERRIDE_acl(val: boolean) {
// 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.
*
@@ -139,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.
*
@@ -148,32 +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;
+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)
+}
- // 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;
+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],
@@ -181,17 +184,22 @@ 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 = key.substring(4).replace('_', '.'); // an individual or a group
- if (currentUserGroups.includes(entity) || userChecked === entity) {
- if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+ 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) {
effectiveAcl = value as symbol;
- if (effectiveAcl === AclAdmin) break;
}
}
}
+
+ // 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;
+
// 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;
}
@@ -213,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]
]);
@@ -223,14 +231,17 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
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 (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) {
+ if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) {
target[key] = acl;
layoutDocChanged = true;
}
if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
- dataDoc[key] = acl;
- dataDocChanged = true;
+
+ if (GetEffectiveAcl(dataDoc) === AclAdmin) {
+ dataDoc[key] = acl;
+ dataDocChanged = true;
+ }
// maps over the aliases of the document
const links = DocListCast(dataDoc.links);
@@ -238,41 +249,41 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
// maps over the children of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => {
- if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- }
+ // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, d, inheritingFromCollection, visited);
+ // }
const data = d[DataSym];
- if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
distributeAcls(key, acl, data, inheritingFromCollection, visited);
}
});
// maps over the annotations of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => {
- if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- }
+ // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, d, inheritingFromCollection, visited);
+ // }
const data = d[DataSym];
- if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
distributeAcls(key, acl, data, inheritingFromCollection, visited);
}
});
}
- 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].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") && (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("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
@@ -293,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);
@@ -354,20 +363,61 @@ 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);
+ let lastValue = ObjectField.MakeCopy(value);
return (diff?: any) => {
- if (true || !diff) {
- diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
- const oldValue = current;
- const newValue = ObjectField.MakeCopy(value);
- current = newValue;
- if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) {
- UndoManager.AddEvent({
- redo() { receiver[prop] = newValue; },
- undo() { receiver[prop] = oldValue; }
- });
- }
+ 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 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(() => {
+ 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]);
+ })
+ } :
+ 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: () => {
+ 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]);
+ }
+ }
+ : {
+ redo: () => {
+ receiver[prop] = ObjectField.MakeCopy(newValue as List<any>);
+ lastValue = ObjectField.MakeCopy(receiver[prop]);
+ },
+ undo: () => {
+ receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
+ lastValue = ObjectField.MakeCopy(receiver[prop]);
+ }
+ });
}
- target[Update](diff);
+ target[Update](op);
};
} \ No newline at end of file