aboutsummaryrefslogtreecommitdiff
path: root/src/fields
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields')
-rw-r--r--src/fields/Doc.ts138
-rw-r--r--src/fields/FieldLoader.scss15
-rw-r--r--src/fields/FieldLoader.tsx15
-rw-r--r--src/fields/FieldSymbols.ts24
-rw-r--r--src/fields/List.ts19
-rw-r--r--src/fields/Proxy.ts120
-rw-r--r--src/fields/ScriptField.ts16
-rw-r--r--src/fields/documentSchemas.ts2
-rw-r--r--src/fields/util.ts173
9 files changed, 273 insertions, 249 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index fc43325fe..df49c32f0 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { saveAs } from 'file-saver';
-import { action, computed, observable, ObservableMap, runInAction } from 'mobx';
+import { action, computed, observable, ObservableMap, ObservableSet, runInAction } from 'mobx';
import { computedFn } from 'mobx-utils';
import { alias, map, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
@@ -94,6 +94,8 @@ export function DocListCastOrNull(field: FieldResult) {
export const WidthSym = Symbol('Width');
export const HeightSym = Symbol('Height');
+export const AnimationSym = Symbol('Animation');
+export const HighlightSym = Symbol('Highlight');
export const DataSym = Symbol('Data');
export const LayoutSym = Symbol('Layout');
export const FieldsSym = Symbol('Fields');
@@ -111,28 +113,37 @@ export const Initializing = Symbol('Initializing');
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.Augment, AclAugment],
- [SharingPermissions.SelfEdit, AclSelfEdit],
- [SharingPermissions.Edit, AclEdit],
- [SharingPermissions.Admin, AclAdmin],
+export enum aclLevel {
+ unset = -1,
+ unshared = 0,
+ viewable = 1,
+ augmentable = 2,
+ selfEditable = 2.5,
+ editable = 3,
+ admin = 4,
+}
+// prettier-ignore
+export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([
+ [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }],
+ [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }],
+ [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}],
+ [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }],
+ [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }],
+ [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }],
+ [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }],
]);
+export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }]));
// 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;
+ const target = (doc as any)?.__fields ?? doc;
+ const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {};
+ Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl));
+ if (Object.keys(permissions).length || doc[AclSym]?.length) {
+ runInAction(() => (doc[AclSym] = permissions));
}
if (doc.proto instanceof Promise) {
@@ -228,6 +239,19 @@ export class Doc extends RefField {
public static get MyFileOrphans() {
return DocCast(Doc.UserDoc().myFileOrphans);
}
+ public static AddFileOrphan(doc: Doc) {
+ if (
+ doc &&
+ Doc.MyFileOrphans instanceof Doc &&
+ Doc.IsPrototype(doc) &&
+ !Doc.IsSystem(doc) &&
+ ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
+ !doc.isFolder &&
+ !doc.annotationOn
+ ) {
+ Doc.AddDocToList(Doc.MyFileOrphans, undefined, doc);
+ }
+ }
public static get MyTools() {
return DocCast(Doc.UserDoc().myTools);
}
@@ -265,7 +289,7 @@ export class Doc extends RefField {
}
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
- const doc = new Proxy<this>(this, {
+ const docProxy = new Proxy<this>(this, {
set: setter,
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
@@ -294,11 +318,11 @@ export class Doc extends RefField {
throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
},
});
- this[SelfProxy] = doc;
+ this[SelfProxy] = docProxy;
if (!id || forceSave) {
- DocServer.CreateField(doc);
+ DocServer.CreateField(docProxy);
}
- return doc;
+ return docProxy;
}
proto: Opt<Doc>;
@@ -327,8 +351,15 @@ export class Doc extends RefField {
@observable private ___fields: any = {};
@observable private ___fieldKeys: any = {};
+ /// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing
@observable public [AclSym]: { [key: string]: symbol } = {};
@observable public [DirectLinksSym]: Set<Doc> = new Set();
+ @observable public [AnimationSym]: Opt<Doc>;
+ @observable public [HighlightSym]: boolean = false;
+ static __Anim(Doc: Doc) {
+ // for debugging to print AnimationSym field easily.
+ return Doc[AnimationSym];
+ }
private [UpdatingFromServer]: boolean = false;
private [ForceServerWrite]: boolean = false;
@@ -756,12 +787,13 @@ export namespace Doc {
}
cloneMap.set(doc[Id], copy);
}
+ Doc.AddFileOrphan(copy);
return copy;
}
export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
const linkMap = new Map<Doc, Doc>();
const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
- const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], dontCreate, asBranch);
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf', 'context'], dontCreate, asBranch);
Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
@@ -934,11 +966,12 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
const copy = new Doc(copyProtoId, true);
+ updateCachedAcls(copy);
const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []);
Object.keys(doc).forEach(key => {
if (exclude.includes(key)) return;
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- const field = ProxyField.WithoutProxy(() => doc[key]);
+ const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
if (key === 'proto' && copyProto) {
if (doc[key] instanceof Doc) {
copy[key] = Doc.MakeCopy(doc[key]!, false);
@@ -962,7 +995,6 @@ export namespace Doc {
}
}
});
- copy.author = Doc.CurrentUserEmail;
if (copyProto) {
Doc.GetProto(copy).context = undefined;
Doc.GetProto(copy).aliases = new List<Doc>([copy]);
@@ -974,6 +1006,7 @@ export namespace Doc {
if (retitle) {
copy.title = incrementTitleCopy(StrCast(copy.title));
}
+ Doc.AddFileOrphan(copy);
return copy;
}
@@ -983,6 +1016,7 @@ export namespace Doc {
if (doc) {
const delegate = new Doc(id, true);
delegate[Initializing] = true;
+ updateCachedAcls(delegate);
delegate.proto = doc;
delegate.author = Doc.CurrentUserEmail;
Object.keys(doc)
@@ -991,6 +1025,7 @@ export namespace Doc {
if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', delegate);
title && (delegate.title = title);
delegate[Initializing] = false;
+ Doc.AddFileOrphan(delegate);
return delegate;
}
return undefined;
@@ -1113,7 +1148,7 @@ export namespace Doc {
BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
}
- const brushManager = new DocBrush();
+ export const brushManager = new DocBrush();
export class DocData {
@observable _user_doc: Doc = undefined!;
@@ -1259,48 +1294,55 @@ export namespace Doc {
}
export function linkFollowUnhighlight() {
- Doc.UnhighlightAll();
+ UnhighlightWatchers.forEach(watcher => watcher());
+ UnhighlightWatchers.length = 0;
+ highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
}
- let _lastDate = 0;
- export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) {
+ let UnhighlightWatchers: (() => void)[] = [];
+ export let UnhighlightTimer: any;
+ export function AddUnHighlightWatcher(watcher: () => void) {
+ if (UnhighlightTimer) {
+ UnhighlightWatchers.push(watcher);
+ } else watcher();
+ }
+ export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
linkFollowUnhighlight();
- (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
+ (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
document.addEventListener('pointerdown', linkFollowUnhighlight);
- const lastDate = (_lastDate = Date.now());
- window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
+ if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
+ UnhighlightTimer = window.setTimeout(() => {
+ linkFollowUnhighlight();
+ UnhighlightTimer = 0;
+ }, 5000);
}
- export class HighlightBrush {
- @observable HighlightedDoc: Map<Doc, boolean> = new Map();
- }
- const highlightManager = new HighlightBrush();
+ export var highlightedDocs = new ObservableSet<Doc>();
export function IsHighlighted(doc: Doc) {
if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
- return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
+ return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
}
- export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
+ export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
runInAction(() => {
- highlightManager.HighlightedDoc.set(doc, true);
- dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
+ highlightedDocs.add(doc);
+ doc[HighlightSym] = true;
+ doc[AnimationSym] = presEffect;
+ if (dataAndDisplayDocs) {
+ highlightedDocs.add(Doc.GetProto(doc));
+ Doc.GetProto(doc)[HighlightSym] = true;
+ }
});
}
export function UnHighlightDoc(doc: Doc) {
runInAction(() => {
- highlightManager.HighlightedDoc.set(doc, false);
- highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
+ highlightedDocs.delete(doc);
+ highlightedDocs.delete(Doc.GetProto(doc));
+ doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
+ doc[AnimationSym] = undefined;
});
}
- export function UnhighlightAll() {
- const mapEntries = highlightManager.HighlightedDoc.keys();
- let docEntry: IteratorResult<Doc>;
- while (!(docEntry = mapEntries.next()).done) {
- const targetDoc = docEntry.value;
- targetDoc && Doc.UnHighlightDoc(targetDoc);
- }
- }
export function UnBrushAllDocs() {
brushManager.BrushedDoc.clear();
}
diff --git a/src/fields/FieldLoader.scss b/src/fields/FieldLoader.scss
new file mode 100644
index 000000000..9a23c3e4f
--- /dev/null
+++ b/src/fields/FieldLoader.scss
@@ -0,0 +1,15 @@
+.fieldLoader {
+ z-index: 10000;
+ width: 200px;
+ height: 30;
+ background: lightblue;
+ position: absolute;
+ left: calc(50% - 99px);
+ top: calc(50% + 110px);
+ display: flex;
+ align-items: center;
+ padding: 20px;
+ margin: auto;
+ display: 'block';
+ box-shadow: darkslategrey 0.2vw 0.2vw 0.8vw;
+}
diff --git a/src/fields/FieldLoader.tsx b/src/fields/FieldLoader.tsx
new file mode 100644
index 000000000..2a7b936f7
--- /dev/null
+++ b/src/fields/FieldLoader.tsx
@@ -0,0 +1,15 @@
+import { observable } from 'mobx';
+import { observer } from 'mobx-react';
+
+import * as React from 'react';
+import './FieldLoader.scss';
+
+@observer
+export class FieldLoader extends React.Component {
+ @observable public static ServerLoadStatus = { requested: 0, retrieved: 0 };
+ public static active = false;
+
+ render() {
+ return <div className="fieldLoader">{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
+ }
+}
diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts
index 8d040f493..e50c2856f 100644
--- a/src/fields/FieldSymbols.ts
+++ b/src/fields/FieldSymbols.ts
@@ -1,12 +1,12 @@
-
-export const Update = Symbol("Update");
-export const Self = Symbol("Self");
-export const SelfProxy = Symbol("SelfProxy");
-export const HandleUpdate = Symbol("HandleUpdate");
-export const Id = Symbol("Id");
-export const OnUpdate = Symbol("OnUpdate");
-export const Parent = Symbol("Parent");
-export const Copy = Symbol("Copy");
-export const ToScriptString = Symbol("ToScriptString");
-export const ToPlainText = Symbol("ToPlainText");
-export const ToString = Symbol("ToString");
+export const Update = Symbol('Update');
+export const Self = Symbol('Self');
+export const SelfProxy = Symbol('SelfProxy');
+export const HandleUpdate = Symbol('HandleUpdate');
+export const Id = Symbol('Id');
+export const OnUpdate = Symbol('OnUpdate');
+export const Parent = Symbol('Parent');
+export const Copy = Symbol('Copy');
+export const ToValue = Symbol('ToValue');
+export const ToScriptString = Symbol('ToScriptString');
+export const ToPlainText = Symbol('ToPlainText');
+export const ToString = Symbol('ToString');
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 5cc4ca543..9c7794813 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -127,6 +127,9 @@ const listHandlers: any = {
this[Self].__realFields();
return this[Self].__fields.map(toRealField).join(separator);
},
+ lastElement() {
+ return this[Self].__realFields().lastElement();
+ },
lastIndexOf(valueToFind: any, fromIndex: number) {
if (valueToFind instanceof RefField) {
return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
@@ -210,10 +213,10 @@ function toObjectField(field: Field) {
}
function toRealField(field: Field) {
- return field instanceof ProxyField ? field.value() : field;
+ return field instanceof ProxyField ? field.value : field;
}
-function listGetter(target: any, prop: string | number | symbol, receiver: any): any {
+function listGetter(target: any, prop: string | symbol, receiver: any): any {
if (listHandlers.hasOwnProperty(prop)) {
return listHandlers[prop];
}
@@ -271,19 +274,17 @@ class ListImpl<T extends Field> extends ObjectField {
// this requests all ProxyFields at the same time to avoid the overhead
// of separate network requests and separate updates to the React dom.
private __realFields() {
- const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: f instanceof ProxyField ? f.promisedValue() : '' }));
+ const unrequested = this.__fields.filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>);
// if we find any ProxyFields that don't have a current value, then
// start the server request for all of them
- if (promised.length) {
- const batchPromise = DocServer.GetRefFields(promised.map(p => p.promisedFieldId));
+ if (unrequested.length) {
+ const batchPromise = DocServer.GetRefFields(unrequested.map(p => p.fieldId));
// as soon as we get the fields from the server, set all the list values in one
// action to generate one React dom update.
- batchPromise.then(pfields => promised.forEach(p => p.field.setValue(pfields[p.promisedFieldId])));
+ const allSetPromise = batchPromise.then(action(pfields => unrequested.map(toReq => toReq.setValue(pfields[toReq.fieldId]))));
// we also have to mark all lists items with this promise so that any calls to them
// will await the batch request and return the requested field value.
- // This assumes the handler for 'promise' in the call above being invoked before the
- // handler for 'promise' in the lines below.
- promised.forEach(p => p.field.setPromise(batchPromise.then(pfields => pfields[p.promisedFieldId])));
+ unrequested.forEach(p => p.setExternalValuePromise(allSetPromise));
}
return this.__fields.map(toRealField);
}
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 2c5f38818..55d1d9ea4 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -1,95 +1,90 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { FieldWaiting } from "./Doc";
-import { primitive, serializable } from "serializr";
-import { observable, action, runInAction } from "mobx";
-import { DocServer } from "../client/DocServer";
-import { RefField } from "./RefField";
-import { ObjectField } from "./ObjectField";
-import { Id, Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { Plugins } from "./util";
+import { Deserializable } from '../client/util/SerializationHelper';
+import { FieldWaiting, Opt } from './Doc';
+import { primitive, serializable } from 'serializr';
+import { observable, action, runInAction, computed } from 'mobx';
+import { DocServer } from '../client/DocServer';
+import { RefField } from './RefField';
+import { ObjectField } from './ObjectField';
+import { Id, Copy, ToScriptString, ToString, ToValue } from './FieldSymbols';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
function deserializeProxy(field: any) {
- if (!field.cache) {
- field.cache = DocServer.GetCachedRefField(field.fieldId) as any;
+ if (!field.cache.field) {
+ field.cache = { field: DocServer.GetCachedRefField(field.fieldId) as any, p: undefined };
}
}
-@Deserializable("proxy", deserializeProxy)
+@Deserializable('proxy', deserializeProxy)
export class ProxyField<T extends RefField> extends ObjectField {
constructor();
constructor(value: T);
constructor(fieldId: string);
constructor(value?: T | string) {
super();
- if (typeof value === "string") {
- this.cache = DocServer.GetCachedRefField(value) as any;
+ if (typeof value === 'string') {
+ //this.cache = DocServer.GetCachedRefField(value) as any;
this.fieldId = value;
} else if (value) {
- this.cache = value;
+ this.cache = { field: value, p: undefined };
this.fieldId = value[Id];
}
}
+ [ToValue](doc: any) {
+ return ProxyField.toValue(this);
+ }
+
[Copy]() {
- if (this.cache) return new ProxyField<T>(this.cache);
+ if (this.cache.field) return new ProxyField<T>(this.cache.field);
return new ProxyField<T>(this.fieldId);
}
[ToScriptString]() {
- return "invalid";
+ return 'invalid';
}
[ToString]() {
- return "ProxyField";
+ return 'ProxyField';
}
@serializable(primitive())
- readonly fieldId: string = "";
+ readonly fieldId: string = '';
- // This getter/setter and nested object thing is
+ // This getter/setter and nested object thing is
// because mobx doesn't play well with observable proxies
@observable.ref
- private _cache: { readonly field: T | undefined } = { field: undefined };
- private get cache(): T | undefined {
- return this._cache.field;
+ private _cache: { readonly field: T | undefined; p: FieldWaiting<T> | undefined } = { field: undefined, p: undefined };
+ private get cache(): { field: T | undefined; p: FieldWaiting<T> | undefined } {
+ return this._cache;
}
- private set cache(field: T | undefined) {
- this._cache = { field };
+ private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) {
+ runInAction(() => (this._cache = { ...val }));
}
private failed = false;
- private promise?: Promise<any>;
- value(): T | undefined | FieldWaiting<T> {
- if (this.cache) {
- return this.cache;
- }
- if (this.failed) {
- return undefined;
- }
- if (!this.promise) {
- const cached = DocServer.GetCachedRefField(this.fieldId);
- if (cached !== undefined) {
- runInAction(() => this.cache = cached as any);
- return cached as any;
- }
- this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
- this.promise = undefined;
- this.cache = field;
- if (field === undefined) this.failed = true;
- return field;
- }));
+ @computed get value(): T | undefined | FieldWaiting<T> {
+ if (this.cache.field) return this.cache.field;
+ if (this.failed) return undefined;
+
+ this.cache.field = DocServer.GetCachedRefField(this.fieldId) as T;
+ if (!this.cache.field && !this.cache.p) {
+ this.cache = {
+ field: undefined,
+ p: DocServer.GetRefField(this.fieldId).then(val => this.setValue(val as T)) as FieldWaiting<T>,
+ };
}
- return DocServer.GetCachedRefField(this.fieldId) ?? (this.promise as any);
+ return this.cache.field ?? this.cache.p;
}
- promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; }
- setPromise(promise: any) {
- this.promise = promise;
+ @computed get needsRequesting(): boolean {
+ return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false;
+ }
+
+ setExternalValuePromise(externalValuePromise: Promise<any>) {
+ this.cache.p = externalValuePromise.then(() => this.value) as FieldWaiting<T>;
}
@action
- setValue(field: any) {
- this.promise = undefined;
- this.cache = field;
- if (field === undefined) this.failed = true;
+ setValue(field: Opt<T>) {
+ this.cache = { field, p: undefined };
+ this.failed = field === undefined;
return field;
}
}
@@ -113,20 +108,17 @@ export namespace ProxyField {
}
}
- export function initPlugin() {
- Plugins.addGetterPlugin((doc, _, value) => {
- if (useProxy && value instanceof ProxyField) {
- return { value: value.value() };
- }
- });
+ export function toValue(value: any) {
+ if (useProxy) {
+ return { value: value.value };
+ }
}
}
function prefetchValue(proxy: PrefetchProxy<RefField>) {
- return proxy.value() as any;
+ return proxy.value as any;
}
@scriptingGlobal
-@Deserializable("prefetch_proxy", prefetchValue)
-export class PrefetchProxy<T extends RefField> extends ProxyField<T> {
-}
+@Deserializable('prefetch_proxy', prefetchValue)
+export class PrefetchProxy<T extends RefField> extends ProxyField<T> {}
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 4896c027d..b23732b45 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -6,11 +6,10 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
import { autoObject, Deserializable } from '../client/util/SerializationHelper';
import { numberRange } from '../Utils';
import { Doc, Field, Opt } from './Doc';
-import { Copy, Id, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
import { Cast, StrCast } from './Types';
-import { Plugins } from './util';
function optional(propSchema: PropSchema) {
return custom(
@@ -175,6 +174,9 @@ export class ComputedField extends ScriptField {
value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
_valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
+ [ToValue](doc: Doc) {
+ return ComputedField.toValue(doc, this);
+ }
[Copy](): ObjectField {
return new ComputedField(this.script, this.setterscript, this.rawscript);
}
@@ -239,12 +241,10 @@ export namespace ComputedField {
}
}
- export function initPlugin() {
- Plugins.addGetterPlugin((doc, _, value) => {
- if (useComputed && value instanceof ComputedField) {
- return { value: value._valueOutsideReaction(doc), shouldReturn: true };
- }
- });
+ export function toValue(doc: any, value: any) {
+ if (useComputed) {
+ return { value: value._valueOutsideReaction(doc) };
+ }
}
}
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 24b5a359d..10324449f 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -93,7 +93,7 @@ export const documentSchema = createSchema({
layers: listSpec('string'), // which layers the document is part of
_lockedPosition: 'boolean', // whether the document can be moved (dragged)
_lockedTransform: 'boolean', // whether a freeformview can pan/zoom
- displayArrow: 'boolean', // toggles directed arrows
+ linkDisplayArrow: 'boolean', // toggles directed arrows
// drag drop properties
_stayInCollection: 'boolean', // whether document can be dropped into a different collection
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 4a62a6a1f..dc0b41276 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,21 +1,18 @@
-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 { SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
-import { CollectionDockingView } from '../client/views/collections/CollectionDockingView';
import { returnZero } from '../Utils';
import CursorField from './CursorField';
import {
AclAdmin,
- AclAugment,
AclEdit,
+ aclLevel,
AclPrivate,
- AclReadonly,
AclSelfEdit,
AclSym,
- AclUnset,
DataSym,
Doc,
DocListCast,
@@ -23,13 +20,15 @@ import {
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';
@@ -48,19 +47,6 @@ 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()) {
target[prop] = value;
@@ -119,6 +105,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
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);
}
@@ -126,7 +113,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;
@@ -183,8 +175,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',
@@ -204,22 +199,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);
@@ -231,42 +220,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;
}
/**
@@ -291,21 +270,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
}
visited.push(target);
- const HierarchyMapping = new Map<string, number>([
- ['Not Shared', 0],
- ['Can View', 1],
- ['Can Augment', 2],
- ['Self Edit', 2.5],
- ['Can Edit', 3],
- ['Admin', 4],
- ]);
-
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;
@@ -316,15 +283,16 @@ 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));
+ DocListCast(dataDoc.links).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
// maps over the children of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => {
@@ -353,10 +321,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') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), 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('_')) {
@@ -372,53 +340,44 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
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) {
@@ -450,7 +409,7 @@ 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(() => {
@@ -460,7 +419,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
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.value() : item);
+ const ind = receiver[prop].indexOf(item.value ?? item);
ind !== -1 && receiver[prop].splice(ind, 1);
}
});
@@ -472,7 +431,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]);
@@ -484,8 +443,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]);