diff options
Diffstat (limited to 'src/fields/Doc.ts')
-rw-r--r-- | src/fields/Doc.ts | 173 |
1 files changed, 86 insertions, 87 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); } |