aboutsummaryrefslogtreecommitdiff
path: root/src/fields
diff options
context:
space:
mode:
Diffstat (limited to 'src/fields')
-rw-r--r--src/fields/Doc.ts218
-rw-r--r--src/fields/Proxy.ts8
-rw-r--r--src/fields/Schema.ts4
-rw-r--r--src/fields/ScriptField.ts26
-rw-r--r--src/fields/documentSchemas.ts10
-rw-r--r--src/fields/util.ts136
6 files changed, 320 insertions, 82 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index b2fc10b99..b78d5611c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
-import { action, computed, observable, ObservableMap, runInAction } from "mobx";
+import { action, computed, observable, ObservableMap, runInAction, untracked } from "mobx";
import { computedFn } from "mobx-utils";
-import { alias, map, serializable } from "serializr";
+import { alias, map, serializable, list } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
@@ -14,11 +14,15 @@ import { ObjectField } from "./ObjectField";
import { PrefetchProxy, ProxyField } from "./Proxy";
import { FieldId, RefField } from "./RefField";
import { RichTextField } from "./RichTextField";
+import { ImageField, VideoField, WebField, AudioField, PdfField } from "./URLField";
+import { DateField } from "./DateField";
import { listSpec } from "./Schema";
-import { ComputedField } from "./ScriptField";
+import { ComputedField, ScriptField } from "./ScriptField";
import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
-import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions } from "./util";
import { LinkManager } from "../client/util/LinkManager";
+import JSZip = require("jszip");
+import { saveAs } from "file-saver";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -92,31 +96,33 @@ export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
export const DataSym = Symbol("Data");
export const LayoutSym = Symbol("Layout");
+export const FieldsSym = Symbol("Fields");
export const AclSym = Symbol("Acl");
export const AclPrivate = Symbol("AclOwnerOnly");
export const AclReadonly = Symbol("AclReadOnly");
export const AclAddonly = Symbol("AclAddonly");
-export const AclReadWrite = Symbol("AclReadWrite");
+export const AclEdit = Symbol("AclEdit");
+export const AclAdmin = Symbol("AclAdmin");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
-const CachedUpdates = Symbol("Cached updates");
+export const CachedUpdates = Symbol("Cached updates");
+const AclMap = new Map<string, symbol>([
+ [SharingPermissions.None, AclPrivate],
+ [SharingPermissions.View, AclReadonly],
+ [SharingPermissions.Add, AclAddonly],
+ [SharingPermissions.Edit, AclEdit],
+ [SharingPermissions.Admin, AclAdmin]
+]);
export function fetchProto(doc: Doc) {
- if (doc.author !== Doc.CurrentUserEmail) {
- const acl = Doc.Get(doc, "ACL", true);
- switch (acl) {
- case "ownerOnly":
- doc[AclSym] = AclPrivate;
- return undefined;
- case "readOnly":
- doc[AclSym] = AclReadonly;
- break;
- case "addOnly":
- doc[AclSym] = AclAddonly;
- break;
- case "write":
- doc[AclSym] = AclReadWrite;
- }
+ 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;
+
+ if (GetEffectiveAcl(doc) === AclPrivate) {
+ runInAction(() => doc[FieldsSym](true));
}
if (doc.proto instanceof Promise) {
@@ -134,10 +140,10 @@ export class Doc extends RefField {
set: setter,
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
- has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields,
+ has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields,
ownKeys: target => {
const obj = {} as any;
- if (target[AclSym] !== AclPrivate) Object.assign(obj, target.___fields);
+ if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys);
runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
return Object.keys(obj);
},
@@ -145,11 +151,11 @@ export class Doc extends RefField {
if (prop.toString() === "__LAYOUT__") {
return Reflect.getOwnPropertyDescriptor(target, prop);
}
- if (prop in target.__fields) {
+ if (prop in target.__fieldKeys) {
return {
configurable: true,//TODO Should configurable be true?
enumerable: true,
- value: target.__fields[prop]
+ value: 0//() => target.__fields[prop])
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
@@ -173,16 +179,23 @@ export class Doc extends RefField {
this.___fields = value;
for (const key in value) {
const field = value[key];
+ field && (this.__fieldKeys[key] = true);
if (!(field instanceof ObjectField)) continue;
field[Parent] = this[Self];
field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
}
}
+ private get __fieldKeys() { return this.___fieldKeys; }
+ private set __fieldKeys(value) {
+ this.___fieldKeys = value;
+ }
@observable
- //{ [key: string]: Field | FieldWaiting | undefined }
private ___fields: any = {};
+ @observable
+ private ___fieldKeys: any = {};
+
private [UpdatingFromServer]: boolean = false;
private [Update] = (diff: any) => {
@@ -191,11 +204,19 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
- public [AclSym]: any = undefined;
+ public [FieldsSym] = (clear?: boolean) => {
+ if (clear) {
+ this.___fields = {};
+ this.___fieldKeys = {};
+ }
+ return this.___fields;
+ }
+ @observable
+ public [AclSym]: { [key: string]: symbol };
public [WidthSym] = () => NumCast(this[SelfProxy]._width);
public [HeightSym] = () => NumCast(this[SelfProxy]._height);
public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
- public [ToString]() { return `Doc(${this[AclSym] === AclPrivate ? "-inaccessible-" : this.title})`; }
+ public [ToString]() { return `Doc(${GetEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; }
public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
public get [DataSym]() {
const self = this[SelfProxy];
@@ -215,8 +236,8 @@ export class Doc extends RefField {
return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc;
}
return undefined;
- }
+ }
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
public static CurrentUserEmail: string = "";
@@ -231,11 +252,18 @@ export class Doc extends RefField {
const fKey = key.substring(7);
const fn = async () => {
const value = await SerializationHelper.Deserialize(set[key]);
+ const prev = GetEffectiveAcl(this);
this[UpdatingFromServer] = true;
this[fKey] = value;
+ if (fKey.startsWith("ACL")) {
+ fetchProto(this);
+ }
this[UpdatingFromServer] = false;
+ if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
+ DocServer.GetRefField(this[Id], true);
+ }
};
- if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
+ if (sameAuthor || fKey.startsWith("ACL") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
await fn();
} else {
@@ -483,27 +511,28 @@ export namespace Doc {
return alias;
}
-
-
- export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+ export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean): Promise<Doc> {
if (Doc.IsBaseProto(doc)) return doc;
if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
- const copy = new Doc(undefined, true);
+ const copy = dontCreate ? doc : new Doc(undefined, true);
cloneMap.set(doc[Id], copy);
if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
- const exclude = Cast(doc.excludeFields, listSpec("string"), []);
- Object.keys(doc).forEach(key => {
- if (exclude.includes(key)) return;
+ const filter = Cast(doc.cloneFieldFilter, listSpec("string"), exclusions);
+ await Promise.all(Object.keys(doc).map(async key => {
+ if (filter.includes(key)) return;
+ const assignKey = (val: any) => !dontCreate && (copy[key] = val);
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
- const copyObjectField = (field: ObjectField) => {
- const list = Cast(doc[key], listSpec(Doc));
- if (list !== undefined && !(list instanceof Promise)) {
- copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
+ const copyObjectField = async (field: ObjectField) => {
+ const list = await Cast(doc[key], listSpec(Doc));
+ const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
+ if (docs !== undefined && docs.length) {
+ const clones = await Promise.all(docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions, dontCreate)));
+ !dontCreate && assignKey(new List<Doc>(clones));
} else if (doc[key] instanceof Doc) {
- copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields
+ assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate)); // reference documents except copy documents that are expanded teplate fields
} else {
- copy[key] = ObjectField.MakeCopy(field);
+ assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
rtfs.push({ copy, key, field });
@@ -513,32 +542,34 @@ export namespace Doc {
};
if (key === "proto") {
if (doc[key] instanceof Doc) {
- copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
+ assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate));
}
} else {
if (field instanceof RefField) {
- copy[key] = field;
+ assignKey(field);
} else if (cfield instanceof ComputedField) {
- copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
- (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+ !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
+ (key === "links" && field instanceof ObjectField) && await copyObjectField(field);
} else if (field instanceof ObjectField) {
- copyObjectField(field);
+ await copyObjectField(field);
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
} else {
- copy[key] = field;
+ assignKey(field);
}
}
- });
- Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
- copy.cloneOf = doc;
- cloneMap.set(doc[Id], copy);
+ }));
+ if (!dontCreate) {
+ Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+ copy.cloneOf = doc;
+ cloneMap.set(doc[Id], copy);
+ }
return copy;
}
- export function MakeClone(doc: Doc): Doc {
+ export async function MakeClone(doc: Doc, dontCreate: boolean = false) {
const cloneMap = new Map<string, Doc>();
const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
- const copy = Doc.makeClone(doc, cloneMap, rtfMap);
+ const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"], dontCreate);
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
const mapped = cloneMap.get(id);
@@ -552,9 +583,61 @@ export namespace Doc {
const re = new RegExp(regex, "g");
copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
});
- return copy;
- }
+ return { clone: copy, map: cloneMap };
+ }
+
+ export async function Zip(doc: Doc) {
+ // const a = document.createElement("a");
+ // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+ // a.href = url;
+ // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+ // a.click();
+ const { clone, map } = await Doc.MakeClone(doc, true);
+ function replacer(key: any, value: any) {
+ if (["cloneOf", "context", "cursors"].includes(key)) return undefined;
+ else if (value instanceof Doc) {
+ if (key !== "field" && Number.isNaN(Number(key))) {
+ const __fields = value[FieldsSym]();
+ return { id: value[Id], __type: "Doc", fields: __fields };
+ } else {
+ return { fieldId: value[Id], __type: "proxy" };
+ }
+ }
+ else if (value instanceof ScriptField) return { script: value.script, __type: "script" };
+ else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" };
+ else if (value instanceof ImageField) return { url: value.url.href, __type: "image" };
+ else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" };
+ else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" };
+ else if (value instanceof VideoField) return { url: value.url.href, __type: "video" };
+ else if (value instanceof WebField) return { url: value.url.href, __type: "web" };
+ else if (value instanceof DateField) return { date: value.toString(), __type: "date" };
+ else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" };
+ else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" };
+ else if (value instanceof ComputedField) return { script: value.script, __type: "computed" };
+ else return value;
+ }
+
+ const docs: { [id: string]: any } = {};
+ Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]);
+ const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+
+ var zip = new JSZip();
+ zip.file("doc.json", docString);
+
+ // // Generate a directory within the Zip file structure
+ // var img = zip.folder("images");
+
+ // // Add a file to the directory, in this case an image with data URI as contents
+ // img.file("smile.gif", imgData, {base64: true});
+
+ // Generate the zip file asynchronously
+ zip.generateAsync({ type: "blob" })
+ .then((content: any) => {
+ // Force down of the Zip file
+ saveAs(content, "download.zip");
+ });
+ }
//
// Determines whether the layout needs to be expanded (as a template).
// template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
@@ -657,7 +740,7 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
const copy = new Doc(copyProtoId, true);
- const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+ const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
Object.keys(doc).forEach(key => {
if (exclude.includes(key)) return;
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
@@ -823,7 +906,7 @@ export namespace 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 || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return 0;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
}
export function IsBrushedDegree(doc: Doc) {
@@ -832,13 +915,13 @@ export namespace Doc {
})(doc);
}
export function BrushDoc(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
brushManager.BrushedDoc.set(doc, true);
brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
return doc;
}
export function UnBrushDoc(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
brushManager.BrushedDoc.delete(doc);
brushManager.BrushedDoc.delete(Doc.GetProto(doc));
return doc;
@@ -868,7 +951,7 @@ export namespace Doc {
}
const highlightManager = new HighlightBrush();
export function IsHighlighted(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return false;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
}
export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
@@ -897,9 +980,12 @@ export namespace Doc {
}
export function getDocTemplate(doc?: Doc) {
- return doc?.isTemplateDoc ? doc :
- Cast(doc?.dragFactory, Doc, null)?.isTemplateDoc ? doc?.dragFactory :
- Cast(doc?.layout, Doc, null)?.isTemplateDoc ? doc?.layout : undefined;
+ return !doc ? undefined :
+ doc.isTemplateDoc ? doc :
+ Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory :
+ Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc ?
+ (Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc ? Doc.Layout(doc).proto : Doc.Layout(doc)) :
+ undefined;
}
export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
@@ -1154,7 +1240,7 @@ Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
Scripting.addGlobal(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); });
Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
-Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
+Scripting.addGlobal(function copyField(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; });
Scripting.addGlobal(function aliasDocs(field: any) { return Doc.aliasDocs(field); });
Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
Scripting.addGlobal(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); });
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 555faaad0..62734d3d2 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -9,7 +9,12 @@ import { Id, Copy, ToScriptString, ToString } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
import { Plugins } from "./util";
-@Deserializable("proxy")
+function deserializeProxy(field: any) {
+ if (!field.cache) {
+ field.cache = DocServer.GetCachedRefField(field.fieldId) as any;
+ }
+}
+@Deserializable("proxy", deserializeProxy)
export class ProxyField<T extends RefField> extends ObjectField {
constructor();
constructor(value: T);
@@ -17,6 +22,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
constructor(value?: T | string) {
super();
if (typeof value === "string") {
+ this.cache = DocServer.GetCachedRefField(value) as any;
this.fieldId = value;
} else if (value) {
this.cache = value;
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index 98ef3e087..23ac50f74 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -31,7 +31,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
}
const proto = new Proxy({}, {
get(target: any, prop, receiver) {
- const field = receiver.doc[prop];
+ const field = receiver.doc?.[prop];
if (prop in schema) {
const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
@@ -52,7 +52,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
return field;
},
set(target: any, prop, value, receiver) {
- receiver.doc[prop] = value;
+ receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in ACLs
return true;
}
});
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 11b3b0524..f55483a5b 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -3,7 +3,7 @@ import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions, CompileE
import { Copy, ToScriptString, ToString, Parent, SelfProxy } from "./FieldSymbols";
import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
-import { Doc, Field } from "./Doc";
+import { Doc, Field, Opt } from "./Doc";
import { Plugins, setter } from "./util";
import { computedFn } from "mobx-utils";
import { ProxyField } from "./Proxy";
@@ -38,6 +38,21 @@ const scriptSchema = createSimpleSchema({
});
async function deserializeScript(script: ScriptField) {
+ if (script.script.originalScript === 'getCopy(this.dragFactory, true)') {
+ return (script as any).script = (ScriptField.GetCopyOfDragFactory ?? (ScriptField.GetCopyOfDragFactory = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')))?.script;
+ }
+ if (script.script.originalScript === 'links(self)') {
+ return (script as any).script = (ScriptField.LinksSelf ?? (ScriptField.LinksSelf = ComputedField.MakeFunction('links(self)')))?.script;
+ }
+ if (script.script.originalScript === 'openOnRight(getCopy(this.dragFactory, true))') {
+ return (script as any).script = (ScriptField.OpenOnRight ?? (ScriptField.OpenOnRight = ComputedField.MakeFunction('openOnRight(getCopy(this.dragFactory, true))')))?.script;
+ }
+ if (script.script.originalScript === 'deiconifyView(self)') {
+ return (script as any).script = (ScriptField.DeiconifyView ?? (ScriptField.DeiconifyView = ComputedField.MakeFunction('deiconifyView(self)')))?.script;
+ }
+ if (script.script.originalScript === 'convertToButtons(dragData)') {
+ return (script as any).script = (ScriptField.ConvertToButtons ?? (ScriptField.ConvertToButtons = ComputedField.MakeFunction('convertToButtons(dragData)', { dragData: "DocumentDragData" })))?.script;
+ }
const captures: ProxyField<Doc> = (script as any).captures;
if (captures) {
const doc = (await captures.value())!;
@@ -65,6 +80,11 @@ export class ScriptField extends ObjectField {
@serializable(autoObject())
private captures?: ProxyField<Doc>;
+ public static GetCopyOfDragFactory: Opt<ScriptField>;
+ public static LinksSelf: Opt<ScriptField>;
+ public static OpenOnRight: Opt<ScriptField>;
+ public static DeiconifyView: Opt<ScriptField>;
+ public static ConvertToButtons: Opt<ScriptField>;
constructor(script: CompiledScript, setterscript?: CompiledScript) {
super();
@@ -163,6 +183,10 @@ 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);
}, "returns the value at a given index of a list", "(list: any[], index: number)");
+Scripting.addGlobal(function makeScript(script: string) {
+ return ScriptField.MakeScript(script);
+}, "returns the value at a given index of a list", "(list: any[], index: number)");
+
export namespace ComputedField {
let useComputed = true;
export function DisableComputedFields() {
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 97f62c9d4..8cf8f47b7 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -3,7 +3,6 @@ import { ScriptField } from "./ScriptField";
import { Doc } from "./Doc";
import { DateField } from "./DateField";
import { SchemaHeaderField } from "./SchemaHeaderField";
-import { Schema } from "prosemirror-model";
export const documentSchema = createSchema({
// content properties
@@ -55,7 +54,7 @@ export const documentSchema = createSchema({
_columnsHideIfEmpty: "boolean", // whether empty stacking view column headings should be hidden
_columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
_schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
- _fontSize: "number",
+ _fontSize: "string",
_fontFamily: "string",
_sidebarWidthPercent: "string", // percent of text window width taken up by sidebar
@@ -66,6 +65,7 @@ export const documentSchema = createSchema({
color: "string", // foreground color of document
fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
fontSize: "string",
+ hidden: "boolean", // whether a document should not be displayed
isInkMask: "boolean", // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
@@ -73,6 +73,9 @@ export const documentSchema = createSchema({
opacity: "number", // opacity of document
strokeWidth: "number",
strokeBezier: "number",
+ strokeStartMarker: "string",
+ strokeEndMarker: "string",
+ strokeDash: "string",
textTransform: "string",
treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden
treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
@@ -85,6 +88,9 @@ export const documentSchema = createSchema({
onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab, )
+ hideLinkButton: "boolean", // whether the blue link counter button should be hidden
+ hideAllLinks: "boolean", // whether all individual blue anchor dots should be hidden
+ linkDisplay: "boolean", // whether a link connection should be shown between link anchor endpoints.
isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked
isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee)
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 2dc21c987..a62795e64 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,14 +1,15 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym } 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 } from "./FieldSymbols";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
import { ComputedField } from "./ScriptField";
-import { ScriptCast } from "./Types";
+import { ScriptCast, StrCast } from "./Types";
+import { returnZero } from "../Utils";
function _readOnlySetter(): never {
@@ -34,7 +35,6 @@ export namespace Plugins {
}
const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
- //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
target[prop] = value;
return true;
@@ -67,18 +67,25 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
delete curValue[Parent];
delete curValue[OnUpdate];
}
+
+ const effectiveAcl = GetEffectiveAcl(target);
+
const writeMode = DocServer.getFieldWriteMode(prop as string);
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
- const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly);
- const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default);
+ const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly);
+ const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !playgroundMode;
+
if (writeToDoc) {
if (value === undefined) {
+ target.__fieldKeys && (delete target.__fieldKeys[prop]);
delete target.__fields[prop];
} else {
+ target.__fieldKeys && (target.__fieldKeys[prop] = true);
target.__fields[prop] = value;
}
//if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
+
if (writeToServer) {
if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
@@ -89,8 +96,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
});
+ return true;
}
- return true;
+ return false;
});
let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
@@ -107,11 +115,117 @@ export function OVERRIDE_ACL(val: boolean) {
_overrideAcl = val;
}
+let playgroundMode = false;
+
+export function togglePlaygroundMode() {
+ playgroundMode = !playgroundMode;
+}
+
+export function getPlaygroundMode() {
+ return playgroundMode;
+}
+
+let currentUserGroups: string[] = [];
+
+export function setGroups(groups: string[]) {
+ currentUserGroups = groups;
+}
+
+export enum SharingPermissions {
+ Admin = "Admin",
+ Edit = "Can Edit",
+ Add = "Can Add",
+ View = "Can View",
+ None = "Not Shared"
+}
+
+export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
+ if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
+
+ if (target[AclSym] && Object.keys(target[AclSym]).length) {
+
+ if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin;
+
+ if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
+
+ let effectiveAcl = AclPrivate;
+ let aclPresent = false;
+
+ const HierarchyMapping = new Map<symbol, number>([
+ [AclPrivate, 0],
+ [AclReadonly, 1],
+ [AclAddonly, 2],
+ [AclEdit, 3],
+ [AclAdmin, 4]
+ ]);
+
+ for (const [key, value] of Object.entries(target[AclSym])) {
+ if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) {
+ if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) {
+ aclPresent = true;
+ effectiveAcl = value as symbol;
+ if (effectiveAcl === AclEdit) break;
+ }
+ }
+ }
+ return aclPresent ? effectiveAcl : AclEdit;
+ }
+ return AclAdmin;
+}
+
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) {
+
+ const HierarchyMapping = new Map<string, number>([
+ ["Not Shared", 0],
+ ["Can View", 1],
+ ["Can Add", 2],
+ ["Can Edit", 3],
+ ["Admin", 4]
+ ]);
+
+ const dataDoc = target[DataSym];
+
+ if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl;
+
+ if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
+ dataDoc[key] = acl;
+
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => {
+ if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, d);
+ d[key] = acl;
+ }
+ const data = d[DataSym];
+ if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, data);
+ data[key] = acl;
+ }
+ });
+
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => {
+ if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, d);
+ d[key] = acl;
+ }
+ const data = d[DataSym];
+ if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, data);
+ data[key] = acl;
+ }
+ });
+ }
+}
+
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;
- if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true;
+ const effectiveAcl = GetEffectiveAcl(target, in_prop);
+ if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) 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 Add", "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("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
@@ -130,8 +244,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 === 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 (target[AclSym] === AclPrivate && !_overrideAcl) return undefined;
+ if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
if (prop === LayoutSym) {
return target.__LAYOUT__;
}
@@ -168,7 +284,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP
}
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
- if (proto instanceof Doc && proto[AclSym] !== AclPrivate) {
+ if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
}
return undefined;