diff options
Diffstat (limited to 'src/fields')
| -rw-r--r-- | src/fields/DateField.ts | 14 | ||||
| -rw-r--r-- | src/fields/Doc.ts | 914 | ||||
| -rw-r--r-- | src/fields/DocSymbols.ts | 26 | ||||
| -rw-r--r-- | src/fields/FieldLoader.scss | 15 | ||||
| -rw-r--r-- | src/fields/FieldLoader.tsx | 15 | ||||
| -rw-r--r-- | src/fields/FieldSymbols.ts | 21 | ||||
| -rw-r--r-- | src/fields/IconField.ts | 26 | ||||
| -rw-r--r-- | src/fields/InkField.ts | 5 | ||||
| -rw-r--r-- | src/fields/List.ts | 113 | ||||
| -rw-r--r-- | src/fields/ListSpec.ts | 0 | ||||
| -rw-r--r-- | src/fields/PresField.ts | 6 | ||||
| -rw-r--r-- | src/fields/Proxy.ts | 85 | ||||
| -rw-r--r-- | src/fields/RichTextField.ts | 26 | ||||
| -rw-r--r-- | src/fields/RichTextUtils.ts | 16 | ||||
| -rw-r--r-- | src/fields/Schema.ts | 78 | ||||
| -rw-r--r-- | src/fields/SchemaHeaderField.ts | 7 | ||||
| -rw-r--r-- | src/fields/ScriptField.ts | 65 | ||||
| -rw-r--r-- | src/fields/Types.ts | 96 | ||||
| -rw-r--r-- | src/fields/URLField.ts | 9 | ||||
| -rw-r--r-- | src/fields/documentSchemas.ts | 71 | ||||
| -rw-r--r-- | src/fields/util.ts | 299 |
21 files changed, 976 insertions, 931 deletions
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index 26f51b2d3..2ea619bd9 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -1,11 +1,11 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, date } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGlobals"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, date } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; @scriptingGlobal -@Deserializable("date") +@Deserializable('date') export class DateField extends ObjectField { @serializable(date()) readonly date: Date; @@ -24,7 +24,7 @@ export class DateField extends ObjectField { } [ToScriptString]() { - return `new DateField(new Date(${this.date.toISOString()}))`; + return `new DateField(new Date("${this.date.toISOString()}"))`; } [ToString]() { return this.date.toLocaleString(); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 70cb10970..c9f7e4114 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'; @@ -9,12 +9,42 @@ import { LinkManager } from '../client/util/LinkManager'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { SelectionManager } from '../client/util/SelectionManager'; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; -import { UndoManager } from '../client/util/UndoManager'; +import { undoable, UndoManager } from '../client/util/UndoManager'; +import { decycle } from '../decycler/decycler'; +import * as JSZipUtils from '../JSZipUtils'; import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils'; import { DateField } from './DateField'; -import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; -import { InkTool } from './InkField'; -import { List } from './List'; +import { + AclAdmin, + AclAugment, + AclEdit, + AclPrivate, + AclReadonly, + AclSelfEdit, + AclUnset, + Animation, + CachedUpdates, + DirectLinks, + DocAcl, + DocCss, + DocData, + DocFields, + DocLayout, + FieldKeys, + FieldTuples, + ForceServerWrite, + Height, + Highlight, + Initializing, + Self, + SelfProxy, + Update, + UpdatingFromServer, + Width, +} from './DocSymbols'; +import { Copy, HandleUpdate, Id, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { InkField, InkTool } from './InkField'; +import { List, ListFieldName } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { FieldId, RefField } from './RefField'; @@ -22,31 +52,33 @@ import { RichTextField } from './RichTextField'; import { listSpec } from './Schema'; import { ComputedField, ScriptField } from './ScriptField'; import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; -import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField'; +import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util'; import JSZip = require('jszip'); - export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { - const onDelegate = Object.keys(doc).includes(key); + const onDelegate = Object.keys(doc).includes(key.replace(/^_/, '')); const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); + return !Field.IsField(field) + ? key.startsWith('_') + ? '=' + : '' + : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field)); } export function toScriptString(field: Field): string { - if (typeof field === 'string') { - if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s - return `"${field}"`; + switch (typeof field) { + case 'string': + if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s + return !field.includes('`') ? `\`${field}\`` : `"${field}"`; + case 'number': + case 'boolean': + return String(field); } - if (typeof field === 'number' || typeof field === 'boolean') return String(field); - if (field === undefined || field === null) return 'null'; - return field[ToScriptString](); + return field?.[ToScriptString]?.() ?? 'null'; } export function toString(field: Field): string { - 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 ''; + if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field); + return field?.[ToString]?.() || ''; } export function IsField(field: any): field is Field; export function IsField(field: any, includeUndefined: true): field is Field | undefined; @@ -79,80 +111,58 @@ export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); } -export function NumListCast(field: FieldResult) { - return Cast(field, listSpec('number'), []); -} -export function StrListCast(field: FieldResult) { - return Cast(field, listSpec('string'), []); +export function NumListCast(field: FieldResult, defaultVal: number[] = []) { + return Cast(field, listSpec('number'), defaultVal); } -export function DocListCast(field: FieldResult) { - return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; +export function StrListCast(field: FieldResult, defaultVal: string[] = []) { + return Cast(field, listSpec('string'), defaultVal); } -export function DocListCastOrNull(field: FieldResult) { - return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; +export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) { + return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[]; } -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 DirectLinksSym = Symbol('DirectLinks'); -export const AclUnset = Symbol('AclUnset'); -export const AclPrivate = Symbol('AclOwnerOnly'); -export const AclReadonly = Symbol('AclReadOnly'); -export const AclAugment = Symbol('AclAugment'); -export const AclSelfEdit = Symbol('AclSelfEdit'); -export const AclEdit = Symbol('AclEdit'); -export const AclAdmin = Symbol('AclAdmin'); -export const UpdatingFromServer = Symbol('UpdatingFromServer'); -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)?.__fieldTuples ?? 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[DocAcl]?.length) { + runInAction(() => (doc[DocAcl] = permissions)); } if (doc.proto instanceof Promise) { - doc.proto.then(updateCachedAcls); + doc.proto.then(proto => updateCachedAcls(DocCast(proto))); return doc.proto; } } @scriptingGlobal -@Deserializable('Doc', updateCachedAcls).withFields(['id']) +@Deserializable('Doc', updateCachedAcls, ['id']) export class Doc extends RefField { - //TODO tfs: these should be temporary... - private static mainDocId: string | undefined; - public static get MainDocId() { - return this.mainDocId; - } - public static set MainDocId(id: string | undefined) { - this.mainDocId = id; - } - @observable public static CurrentlyLoading: Doc[]; // removes from currently loading display @action @@ -207,6 +217,15 @@ export class Doc extends RefField { public static get MyTrails() { return DocCast(Doc.ActiveDashboard?.myTrails); } + public static IsInMyOverlay(doc: Doc) { + return DocListCast(Doc.MyOverlayDocs?.data).includes(doc); + } + public static AddToMyOverlay(doc: Doc) { + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + } + public static RemFromMyOverlay(doc: Doc) { + Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); + } public static get MyOverlayDocs() { return DocCast(Doc.UserDoc().myOverlayDocs); } @@ -228,6 +247,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.IsDataProto(doc) && + !Doc.IsSystem(doc) && + ![DocumentType.CONFIG, 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,70 +297,67 @@ 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 - has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields, + has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fieldTuples, ownKeys: target => { - const obj = {} as any; - if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys); - runInAction(() => (obj.__LAYOUT__ = target.__LAYOUT__)); - return Object.keys(obj); + const keys = GetEffectiveAcl(target) !== AclPrivate ? Object.keys(target[FieldKeys]) : []; + return [...keys, '__LAYOUT__']; }, getOwnPropertyDescriptor: (target, prop) => { - if (prop.toString() === '__LAYOUT__') { + if (prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) { return Reflect.getOwnPropertyDescriptor(target, prop); } - if (prop in target.__fieldKeys) { - return { - configurable: true, //TODO Should configurable be true? - enumerable: true, - value: 0, //() => target.__fields[prop]) - }; - } - return Reflect.getOwnPropertyDescriptor(target, prop); + return { + configurable: true, //TODO Should configurable be true? + enumerable: true, + value: 0, //() => target.__fieldTuples[prop]) + }; }, deleteProperty: deleteProperty, defineProperty: () => { 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>; [key: string]: FieldResult; @serializable(alias('fields', map(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fields() { - return this.___fields; + private get __fieldTuples() { + return this[FieldTuples]; } - private set __fields(value) { - this.___fields = value; + private set __fieldTuples(value) { + // called by deserializer to set all fields in one shot + this[FieldTuples] = value; for (const key in value) { const field = value[key]; - field !== undefined && (this.__fieldKeys[key] = true); + field !== undefined && (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 private ___fields: any = {}; - @observable private ___fieldKeys: any = {}; - @observable public [AclSym]: { [key: string]: symbol } = {}; - @observable public [DirectLinksSym]: Set<Doc> = new Set(); + @observable private [FieldTuples]: 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 [DocAcl]: { [key: string]: symbol } = {}; + @observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout + @observable public [DirectLinks]: Set<Doc> = new Set(); + @observable public [Animation]: Opt<Doc>; + @observable public [Highlight]: boolean = false; + static __Anim(Doc: Doc) { + // for debugging to print AnimationSym field easily. + return Doc[Animation]; + } private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; @@ -340,15 +369,15 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); - public [WidthSym] = () => NumCast(this[SelfProxy]._width); - public [HeightSym] = () => NumCast(this[SelfProxy]._height); + public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); + public [Width] = () => NumCast(this[SelfProxy]._width); + public [Height] = () => NumCast(this[SelfProxy]._height); public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; - public get [LayoutSym]() { + public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } - public get [DataSym]() { + public get [DocData](): Doc { const self = this[SelfProxy]; return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); } @@ -356,7 +385,7 @@ export class Doc extends RefField { const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null); if (templateLayoutDoc) { let renderFieldKey: any; - const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, 'layout')]; + const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; if (typeof layoutField === 'string') { renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0]; //layoutField.split("'")[1]; } else { @@ -485,40 +514,33 @@ export namespace Doc { export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>; } - export function IsPrototype(doc: Doc) { - return GetT(doc, 'isPrototype', 'boolean', true); + export function IsDataProto(doc: Doc) { + return GetT(doc, 'isDataDoc', 'boolean', true); } export function IsBaseProto(doc: Doc) { - return GetT(doc, 'baseProto', 'boolean', true); + return GetT(doc, 'isBaseProto', 'boolean', true); } export function IsSystem(doc: Doc) { - return GetT(doc, 'system', 'boolean', true); + return GetT(doc, 'isSystem', 'boolean', true); } export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; } export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { if (key.startsWith('_')) key = key.substring(1); - const hasProto = doc.proto instanceof Doc; + const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; - const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1; + const onProto = hasProto && Object.getOwnPropertyNames(hasProto).indexOf(key) !== -1; if (onDeleg || !hasProto || (!onProto && !defaultProto)) { doc[key] = value; - } else doc.proto![key] = value; - } - export async function SetOnPrototype(doc: Doc, key: string, value: Field) { - const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc; - - if (proto) { - proto[key] = value; - } + } else hasProto[key] = value; } export function GetAllPrototypes(doc: Doc): Doc[] { const protos: Doc[] = []; let d: Opt<Doc> = doc; while (d) { protos.push(d); - d = FieldValue(d.proto); + d = DocCast(FieldValue(d.proto)); } return protos; } @@ -551,22 +573,14 @@ export namespace Doc { // compare whether documents or their protos match export function AreProtosEqual(doc?: Doc, other?: Doc) { - if (!doc || !other) return false; - const r = doc === other; - const r2 = Doc.GetProto(doc) === other; - const r3 = Doc.GetProto(other) === doc; - const r4 = Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined; - return r || r2 || r3 || r4; + return doc && other && Doc.GetProto(doc) === Doc.GetProto(other); } // Gets the data document for the document. Note: this is mis-named -- it does not specifically // return the doc's proto, but rather recursively searches through the proto inheritance chain - // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). + // and returns the document who's proto is undefined or whose proto is marked as a data doc ('isDataDoc'). export function GetProto(doc: Doc): Doc { - if (doc instanceof Promise) { - // console.log("GetProto: warning: got Promise insead of Doc"); - } - const proto = doc && (Doc.GetT(doc, 'isPrototype', 'boolean', true) ? doc : doc.proto || doc); + const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : DocCast(doc.proto, doc)); return proto === doc ? proto : Doc.GetProto(proto); } export function GetDataDoc(doc: Doc): Doc { @@ -580,7 +594,7 @@ export namespace Doc { let proto: Doc | undefined = doc; while (proto) { Object.keys(proto).forEach(key => results.add(key)); - proto = proto.proto; + proto = DocCast(FieldValue(proto.proto)); } return Array.from(results); @@ -657,7 +671,7 @@ export namespace Doc { const bounds = docList.reduce( (bounds, doc) => { const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - const [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; + const [bptX, bptY] = [sptX + doc[Width](), sptY + doc[Height]()]; return { x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), @@ -670,100 +684,135 @@ export namespace Doc { return bounds; } - export function MakeAlias(doc: Doc, id?: string) { - const alias = !GetT(doc, 'isPrototype', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); - const layout = Doc.LayoutField(alias); - if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) { - Doc.SetLayout(alias, Doc.MakeAlias(layout)); + export function MakeEmbedding(doc: Doc, id?: string) { + const embedding = !GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); + const layout = Doc.LayoutField(embedding); + if (layout instanceof Doc && layout !== embedding && layout === Doc.Layout(embedding)) { + Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); } - alias.aliasOf = doc; - alias.aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1; - alias.title = ComputedField.MakeFunction(`renameAlias(this)`); - alias.author = Doc.CurrentUserEmail; + embedding.createdFrom = doc; + embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = NumCast(Doc.GetProto(doc).proto_embeddingId) + 1; + embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); + embedding.author = Doc.CurrentUserEmail; - Doc.AddDocToList(Doc.GetProto(doc)[DataSym], 'aliases', alias); + Doc.AddDocToList(Doc.GetProto(doc)[DocData], 'proto_embeddings', embedding); - return alias; + return embedding; } - export function BestAlias(doc: Doc) { - const bestAlias = Doc.GetProto(doc) ? DocListCast(doc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : doc; - return bestAlias ?? Doc.MakeAlias(doc); + export function BestEmbedding(doc: Doc) { + const bestEmbedding = Doc.GetProto(doc) ? DocListCast(doc.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc; + return bestEmbedding ?? Doc.MakeEmbedding(doc); } - export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> { + // this lists out all the tag ids that can be in a RichTextField that might contain document ids. + // if a document is cloned, we need to make sure to clone all of these referenced documents as well; + export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId']; + export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean): Promise<Doc> { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; - const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) || doc : doc) : new Doc(undefined, true); + const copy = new Doc(undefined, true); cloneMap.set(doc[Id], copy); - const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== 'annotationOn') : exclusions; - const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])]; + const filter = [...exclusions, ...StrListCast(doc.cloneFieldFilter)]; await Promise.all( Object.keys(doc).map(async key => { if (filter.includes(key)) return; - const assignKey = (val: any) => !dontCreate && (copy[key] = val); + const assignKey = (val: any) => (copy[key] = val); const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); 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 => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch))); - !dontCreate && assignKey(new List<Doc>(clones)); - } else if (doc[key] instanceof Doc) { - assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks))); + assignKey(new List<Doc>(clones)); } else { - !dontCreate && assignKey(ObjectField.MakeCopy(field)); + assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { - if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) { + if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) { + const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g'); + const rawdocids = field.Data.match(docidsearch); + const docids = rawdocids?.map((str: string) => + DocsInTextFieldIds.reduce((output, exp) => output.replace(new RegExp(`${exp}":`, 'g'), ''), str) + .replace(/"/g, '') + .trim() + ); + const results = docids && (await DocServer.GetRefFields(docids)); + const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key])); + docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); rtfs.push({ copy, key, field }); } } } }; - if (key === 'proto') { - if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); - } - } else if (key === 'anchor1' || key === 'anchor2') { - if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch)); - } - } else { - if (field instanceof RefField) { - assignKey(field); - } else if (cfield instanceof ComputedField) { - !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript)); - } else if (field instanceof ObjectField) { - await copyObjectField(field); - } else if (field instanceof Promise) { - debugger; //This shouldn't happen... + const docAtKey = doc[key]; + if (docAtKey instanceof Doc) { + if (pruneDocs.includes(docAtKey)) { + // prune doc and do nothing + } else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) { + assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); } else { - assignKey(field); + assignKey(docAtKey); } + } else if (field instanceof RefField) { + assignKey(field); + } else if (cfield instanceof ComputedField) { + assignKey(cfield[Copy]()); // ComputedField.MakeFunction(cfield.script.originalScript)); + } else if (field instanceof ObjectField) { + await copyObjectField(field); + } else if (field instanceof Promise) { + debugger; //This shouldn't happen... + } else { + assignKey(field); } }) ); - for (const link of Array.from(doc[DirectLinksSym])) { - const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch); - linkMap.set(link, linkClone); - } - if (!dontCreate) { - Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true); - asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); - if (!Doc.IsPrototype(copy)) { - Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy)); + Array.from(doc[DirectLinks]).forEach(async link => { + if ( + cloneLinks || + ((cloneMap.has(DocCast(link.link_anchor_1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id])) && + (cloneMap.has(DocCast(link.link_anchor_2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id]))) + ) { + linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); } - cloneMap.set(doc[Id], copy); - } - Doc.IsPrototype(copy) && Doc.AddDocToList(Doc.MyFileOrphans, undefined, copy); + }); + Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); + copy.cloneOf = 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>(); + export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) { + if (visited.has(clone)) return; + visited.add(clone); + Object.keys(clone) + .filter(key => key !== 'cloneOf') + .map(key => { + const docAtKey = DocCast(clone[key]); + if (docAtKey && !Doc.IsSystem(docAtKey)) { + if (!Array.from(cloneMap.values()).includes(docAtKey)) { + if (cloneMap.has(docAtKey[Id])) { + clone[key] = cloneMap.get(docAtKey[Id]); + } else clone[key] = undefined; + } else { + repairClone(docAtKey, cloneMap, visited); + } + } + }); + } + export function MakeClones(docs: Doc[], cloneLinks: boolean) { + const cloneMap = new Map<string, Doc>(); + return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap)); + } + + export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) { + const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - 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)); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks); + const repaired = new Set<Doc>(); + const linkedDocs = Array.from(linkMap.values()); + linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); @@ -773,70 +822,80 @@ export namespace Doc { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; - const regex = `(${Doc.localServerPath()})([^"]*)`; - const re = new RegExp(regex, 'g'); - copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); + const re = new RegExp(`(${Doc.localServerPath()})([^"]*)`, 'g'); + const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g'); + copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); }); - return { clone: copy, map: cloneMap }; + const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; + clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired)); + return { clone: copy, map: cloneMap, linkMap }; } - 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); + export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') { + const { clone, map, linkMap } = await Doc.MakeClone(doc); + const proms = new Set<string>(); function replacer(key: any, value: any) { - if (['branchOf', '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 MapField) return { url: value.url.href, __type: 'map' }; - 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; + if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined; + if (value instanceof ImageField) { + const extension = value.url.href.replace(/.*\./, ''); + proms.add(value.url.href.replace('.' + extension, '_o.' + extension)); + return SerializationHelper.Serialize(value); + } + if (value instanceof PdfField || value instanceof AudioField || value instanceof VideoField) { + proms.add(value.url.href); + return SerializationHelper.Serialize(value); + } + if ( + value instanceof Doc || + value instanceof ScriptField || + value instanceof RichTextField || + value instanceof InkField || + value instanceof CsvField || + value instanceof WebField || + value instanceof DateField || + value instanceof ProxyField || + value instanceof ComputedField + ) { + return SerializationHelper.Serialize(value); + } + if (value instanceof Array && key !== ListFieldName && key !== InkField.InkDataFieldName) return { fields: value, __type: 'list' }; + return value; } const docs: { [id: string]: any } = {}; + const links: { [id: string]: any } = {}; Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1])); - const docString = JSON.stringify({ id: doc[Id], docs }, replacer); + Array.from(linkMap.entries()).forEach(l => (links[l[0]] = l[1])); + const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer)); const 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, doc.title + '.zip'); // glr: Possibly change the name of the document to match the title? - }); - } - // - // 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 - // - export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) { - return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc; + var count = 0; + const promArr = Array.from(proms).filter(url => url.startsWith(window.location.origin)); + if (!promArr.length) { + zip.file('docs.json', jsonDocs); + zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename)); + } else + promArr.forEach((url, i) => { + // loading a file and add it in a zip file + JSZipUtils.getBinaryContent(url, (err: any, data: any) => { + if (err) throw err; // or handle the error + // // Generate a directory within the Zip file structure + // const assets = zip.folder("assets"); + // assets.file(filename, data, {binary: true}); + const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%'); + zip.file(assetPathOnServer, data, { binary: true }); + console.log(' => ' + url); + if (++count === promArr.length) { + zip.file('docs.json', jsonDocs); + zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename)); + // 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 _pendingMap: Map<string, boolean> = new Map(); @@ -844,44 +903,34 @@ export namespace Doc { // Returns an expanded template layout for a target data document if there is a template relationship // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties // of the original layout while allowing for individual layout properties to be overridden in the expanded layout. - // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key. - // NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field - // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and - // the derefence will then occur on the rootDocument (the original document). - // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as: - // layout_mytemplate(somparam=somearg). - // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument - export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) { - const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace('()', '') || StrCast(templateLayoutDoc.PARAMS); - if ((!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc)) || !targetDoc) return templateLayoutDoc; - - const templateField = StrCast(templateLayoutDoc.isTemplateForField); // the field that the template renders + export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) { + // nothing to do if the layout isn't a template or we don't have a target that's different than the template + if (!targetDoc || templateLayoutDoc === targetDoc || (!templateLayoutDoc.isTemplateForField && !templateLayoutDoc.isTemplateDoc)) { + return templateLayoutDoc; + } + + const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutFieldKey(templateLayoutDoc)); // the field that the template renders // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc // using the template layout doc's id as the field key. // If it doesn't find the expanded layout, then it makes a delegate of the template layout and // saves it on the data doc indexed by the template layout's id. // - const params = args.split('=').length > 1 ? args.split('=')[0] : 'PARAMS'; - const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc); - const expandedLayoutFieldKey = (templateField || layoutFielddKey) + '-layout[' + templateLayoutDoc[Id] + (args ? `(${args})` : '') + ']'; + const expandedLayoutFieldKey = templateField + '-layout[' + templateLayoutDoc[Id] + ']'; let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { expandedTemplateLayout = undefined; _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true); - } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) { - if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) { + } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey)) { + if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc))) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } 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 + templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { - _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true); + _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true); 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; @@ -890,7 +939,7 @@ export namespace Doc { } targetDoc[expandedLayoutFieldKey] = newLayoutDoc; - _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); + _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey); }) ); } @@ -903,11 +952,11 @@ export namespace Doc { // otherwise, it just returns the childDoc export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) { if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) { - console.log('No, no, no!'); + console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc; - return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, '(' + StrCast(containerDoc.PARAMS) + ')'), data: resolvedDataDoc }; + const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc; + return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc }; } export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc { @@ -915,7 +964,7 @@ export namespace Doc { const field = ProxyField.WithoutProxy(() => doc[key]); if (key === 'proto' && copyProto) { if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) { - overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto); + overwrite[key] = Doc.Overwrite(doc.proto, overwrite.proto); } } else { if (field instanceof RefField) { @@ -935,47 +984,46 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = new Doc(copyProtoId, true); - 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]); - if (key === 'proto' && copyProto) { - if (doc[key] instanceof Doc) { - copy[key] = Doc.MakeCopy(doc[key]!, false); - } - } else { - if (field instanceof RefField) { - copy[key] = field; - } else if (cfield instanceof ComputedField) { - copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript); - } else if (field instanceof ObjectField) { - copy[key] = - doc[key] instanceof Doc - ? key.includes('layout[') - ? undefined - : doc[key] // reference documents except remove documents that are expanded teplate fields - : ObjectField.MakeCopy(field); - } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + updateCachedAcls(copy); + const exclude = [...StrListCast(doc.cloneFieldFilter), 'dragFactory_count', 'cloneFieldFilter']; + Object.keys(doc) + .filter(key => !exclude.includes(key)) + .forEach(key => { + if (key === 'proto' && copyProto) { + if (doc.proto instanceof Doc) { + copy[key] = Doc.MakeCopy(doc.proto, false); + } } else { - copy[key] = field; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + if (field instanceof RefField) { + copy[key] = field; + } else if (cfield instanceof ComputedField) { + copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript); + } else if (field instanceof ObjectField) { + copy[key] = + doc[key] instanceof Doc && key.includes('layout[') + ? undefined // remove expanded template field documents + : ObjectField.MakeCopy(field); + } else if (field instanceof Promise) { + debugger; //This shouldn't happend... + } else { + copy[key] = field; + } } - } - }); - copy.author = Doc.CurrentUserEmail; + }); if (copyProto) { - Doc.GetProto(copy).context = undefined; - Doc.GetProto(copy).aliases = new List<Doc>([copy]); + Doc.GetProto(copy).embedContainer = undefined; + Doc.GetProto(copy).proto_embeddings = new List<Doc>([copy]); } else { - Doc.AddDocToList(Doc.GetProto(copy)[DataSym], 'aliases', copy); + Doc.AddDocToList(Doc.GetProto(copy)[DocData], 'proto_embeddings', copy); } - copy.context = undefined; + copy.embedContainer = undefined; Doc.defaultAclPrivate && (copy['acl-Public'] = 'Not Shared'); if (retitle) { copy.title = incrementTitleCopy(StrCast(copy.title)); } - Doc.IsPrototype(copy) && Doc.AddDocToList(Doc.MyFileOrphans, undefined, copy); + Doc.AddFileOrphan(copy); return copy; } @@ -985,14 +1033,16 @@ 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) .filter(key => key.startsWith('acl')) .forEach(key => (delegate[key] = doc[key])); - if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', delegate); + if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DocData], 'proto_embeddings', delegate); title && (delegate.title = title); delegate[Initializing] = false; + Doc.AddFileOrphan(delegate); return delegate; } return undefined; @@ -1007,13 +1057,13 @@ export namespace Doc { delegateProto[Initializing] = true; delegateProto.proto = doc; delegateProto.author = Doc.CurrentUserEmail; - delegateProto.isPrototype = true; + delegateProto.isDataDoc = true; title && (delegateProto.title = title); const delegate = new Doc(id, true); delegate[Initializing] = true; delegate.proto = delegateProto; delegate.author = Doc.CurrentUserEmail; - Doc.AddDocToList(delegateProto[DataSym], 'aliases', delegate); + Doc.AddDocToList(delegateProto[DocData], 'proto_embeddings', delegate); delegate[Initializing] = false; delegateProto[Initializing] = false; return delegate; @@ -1025,9 +1075,9 @@ export namespace Doc { const proto = new Doc(); proto.author = Doc.CurrentUserEmail; const target = Doc.MakeDelegate(proto); - const targetKey = StrCast(templateDoc.layoutKey, 'layout'); + const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); - target.layoutKey = targetKey; + target.layout_fieldKey = targetKey; applied && (Doc.GetProto(applied).type = templateDoc.type); Doc.defaultAclPrivate && (applied['acl-Public'] = 'Not Shared'); return applied; @@ -1115,9 +1165,9 @@ 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 { + export class UserDocData { @observable _user_doc: Doc = undefined!; @observable _sharing_doc: Doc = undefined!; @observable _searchQuery: string = ''; @@ -1127,36 +1177,37 @@ export namespace Doc { // a layout field or 'layout' is given. export function Layout(doc: Doc, layout?: Doc): Doc { const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}-layout[` + layout[Id] + ']'], Doc, null); - return overrideLayout || doc[LayoutSym] || doc; + return overrideLayout || doc[DocLayout] || doc; } export function SetLayout(doc: Doc, layout: Doc | string) { - doc[StrCast(doc.layoutKey, 'layout')] = layout; + doc[StrCast(doc.layout_fieldKey, 'layout')] = layout; } export function LayoutField(doc: Doc) { - return doc[StrCast(doc.layoutKey, 'layout')]; + return doc[StrCast(doc.layout_fieldKey, 'layout')]; } export function LayoutFieldKey(doc: Doc): string { - return StrCast(Doc.Layout(doc).layout).split("'")[1]; + return StrCast(Doc.Layout(doc).layout).split("'")[1]; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey } 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)); + return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeWidth'], useWidth ? doc[Width]() : 0)); } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { - const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0) : 0; - const nheight = doc ? (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]() : 0; - return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight); + if (!doc) return 0; + const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[Height]()) / doc[Width](); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? doc[Height]() : 0); + return NumCast(doc._nativeHeight, nheight || dheight); } export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { - doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width; + doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeWidth'] = width; } export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { - doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeHeight'] = height; + doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeHeight'] = height; } - const manager = new DocData(); + const manager = new UserDocData(); export function SearchQuery(): string { return manager._searchQuery; } @@ -1224,8 +1275,8 @@ export namespace Doc { const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement(); if (lastBrushed) { for (const link of LinkManager.Instance.getAllDirectLinks(lastBrushed)) { - const a1 = Cast(link.anchor1, Doc, null); - const a2 = Cast(link.anchor2, Doc, null); + const a1 = Cast(link.link_anchor_1, Doc, null); + const a2 = Cast(link.link_anchor_2, Doc, null); if (Doc.AreProtosEqual(a1, doc) || Doc.AreProtosEqual(a2, doc) || Doc.AreProtosEqual(Cast(a1.annotationOn, Doc, null), doc) || Doc.AreProtosEqual(Cast(a2.annotationOn, Doc, null), doc)) { return DocBrushStatus.linkHighlighted; } @@ -1257,54 +1308,66 @@ export namespace Doc { } export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { - return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2'; + if (linkDoc.link_anchor_2 === anchorDoc || (linkDoc.link_anchor_2 as Doc).annotationOn) return '2'; + return Doc.AreProtosEqual(anchorDoc, (linkDoc.link_anchor_1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.link_anchor_1 as Doc) ? '1' : '2'; } export function linkFollowUnhighlight() { - Doc.UnhighlightAll(); + clearTimeout(UnhighlightTimer); + 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[Highlight] || Doc.GetProto(doc)[Highlight]; } - 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[Highlight] = true; + doc[Animation] = presEffect; + if (dataAndDisplayDocs) { + highlightedDocs.add(Doc.GetProto(doc)); + Doc.GetProto(doc)[Highlight] = true; + } }); } - export function UnHighlightDoc(doc: Doc) { + /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted + export function UnHighlightDoc(doc?: Doc) { runInAction(() => { - highlightManager.HighlightedDoc.set(doc, false); - highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false); + (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => { + highlightedDocs.delete(doc); + highlightedDocs.delete(Doc.GetProto(doc)); + doc[Highlight] = Doc.GetProto(doc)[Highlight] = false; + doc[Animation] = 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(); + runInAction(() => brushManager.BrushedDoc.clear()); } export function getDocTemplate(doc?: Doc) { @@ -1329,7 +1392,7 @@ export namespace Doc { if (typeof value === 'string') { value = value.replace(`,${Utils.noRecursionHack}`, ''); } - const fieldVal = key === '#' ? (StrCast(doc.tags).includes(':#' + value + ':') ? StrCast(doc.tags) : undefined) : doc[key]; + 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); @@ -1337,70 +1400,70 @@ export namespace Doc { 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); - return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring + return fieldStr.includes(value) || (value === String.fromCharCode(127) + '--undefined--' && fieldVal === undefined); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } export function deiconifyView(doc: Doc) { - StrCast(doc.layoutKey).split('_')[1] === 'icon' && setNativeView(doc); + StrCast(doc.layout_fieldKey).split('_')[1] === 'icon' && setNativeView(doc); } export function setNativeView(doc: any) { - const prevLayout = StrCast(doc.layoutKey).split('_')[1]; + const prevLayout = StrCast(doc.layout_fieldKey).split('_')[1]; const deiconify = prevLayout === 'icon' && StrCast(doc.deiconifyLayout) ? 'layout_' + StrCast(doc.deiconifyLayout) : ''; prevLayout === 'icon' && (doc.deiconifyLayout = undefined); - doc.layoutKey = deiconify || 'layout'; + doc.layout_fieldKey = deiconify || 'layout'; } export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) { if (!container) return; - const docRangeFilters = Cast(container._docRangeFilters, listSpec('string'), []); - for (let i = 0; i < docRangeFilters.length; i += 3) { - if (docRangeFilters[i] === key) { - docRangeFilters.splice(i, 3); + const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []); + for (let i = 0; i < childFiltersByRanges.length; i += 3) { + if (childFiltersByRanges[i] === key) { + childFiltersByRanges.splice(i, 3); break; } } if (range !== undefined) { - docRangeFilters.push(key); - docRangeFilters.push(range[0].toString()); - docRangeFilters.push(range[1].toString()); - container._docRangeFilters = new List<string>(docRangeFilters); + childFiltersByRanges.push(key); + childFiltersByRanges.push(range[0].toString()); + childFiltersByRanges.push(range[1].toString()); + container._childFiltersByRanges = new List<string>(childFiltersByRanges); } } // filters document in a container collection: // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined - export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldSuffix?: string, append: boolean = true) { + export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) { if (!container) return; - const filterField = '_' + (fieldSuffix ? fieldSuffix + '-' : '') + 'docFilters'; - const docFilters = Cast(container[filterField], listSpec('string'), []); + const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters'; + const childFilters = StrListCast(container[filterField]); runInAction(() => { - 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')) { + for (let i = 0; i < childFilters.length; i++) { + const fields = childFilters[i].split(':'); // split key:value:modifier + if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { if (fields[2] === modifiers && modifiers && fields[1] === value) { if (toggle) modifiers = 'remove'; else return; } - docFilters.splice(i, 1); - container[filterField] = new List<string>(docFilters); + childFilters.splice(i, 1); + container[filterField] = new List<string>(childFilters); break; } } - if (!docFilters.length && modifiers === 'match' && value === undefined) { + if (!childFilters.length && modifiers === 'match' && value === undefined) { container[filterField] = undefined; } else if (modifiers !== 'remove') { - !append && (docFilters.length = 0); - docFilters.push(key + ':' + value + ':' + modifiers); - container[filterField] = new List<string>(docFilters); + !append && (childFilters.length = 0); + childFilters.push(key + ':' + value + ':' + modifiers); + container[filterField] = new List<string>(childFilters); } }); } export function readDocRangeFilter(doc: Doc, key: string) { - const docRangeFilters = Cast(doc._docRangeFilters, listSpec('string'), []); - for (let i = 0; i < docRangeFilters.length; i += 3) { - if (docRangeFilters[i] === key) { - return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])]; + const childFiltersByRanges = Cast(doc._childFiltersByRanges, listSpec('string'), []); + for (let i = 0; i < childFiltersByRanges.length; i += 3) { + if (childFiltersByRanges[i] === key) { + return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])]; } } } @@ -1412,18 +1475,18 @@ export namespace Doc { export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) { runInAction(() => { if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) { - layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale; + layoutDoc._freeform_scale = NumCast(layoutDoc._freeform_scale, 1) * contentScale; layoutDoc._nativeWidth = undefined; layoutDoc._nativeHeight = undefined; - layoutDoc._forceReflow = undefined; + layoutDoc._layout_forceReflow = undefined; layoutDoc._nativeHeightUnfrozen = undefined; layoutDoc._nativeDimModifiable = undefined; } else { - layoutDoc._autoHeight = false; + layoutDoc._layout_autoHeight = false; if (!Doc.NativeWidth(layoutDoc)) { layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth); layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight); - layoutDoc._forceReflow = true; + layoutDoc._layout_forceReflow = true; layoutDoc._nativeHeightUnfrozen = true; layoutDoc._nativeDimModifiable = true; } @@ -1451,15 +1514,33 @@ export namespace Doc { return style; } + export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) { + DocServer.GetRefFields(docids).then(async fieldlist => { + const list = Array.from(Object.values(fieldlist)) + .map(d => DocCast(d)) + .filter(d => d); + const docs = clone ? (await Promise.all(Doc.MakeClones(list, false))).map(res => res.clone) : list; + if (ptx !== undefined && pty !== undefined && newPoint !== undefined) { + const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0; + const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0; + docs.map(doc => { + doc.x = NumCast(doc.x) - firstx; + doc.y = NumCast(doc.y) - firsty; + }); + } + undoable(addDocument, 'Paste Doc')(docs); // embedContainer gets set in addDocument + }); + } + // prettier-ignore export function toIcon(doc?: Doc, isOpen?: boolean) { - switch (StrCast(doc?.type)) { + switch (isOpen !== undefined ? DocumentType.COL: StrCast(doc?.type)) { case DocumentType.IMG: return 'image'; case DocumentType.COMPARISON: return 'columns'; case DocumentType.RTF: return 'sticky-note'; case DocumentType.COL: - const folder: IconProp = isOpen ? 'folder-open' : 'folder'; - const chevron: IconProp = isOpen ? 'chevron-down' : 'chevron-right'; + const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : 'question'; + const chevron: IconProp = isOpen === true ? 'chevron-down' : isOpen === false ? 'chevron-right' : 'question'; return !doc?.isFolder ? folder : chevron; case DocumentType.WEB: return 'globe-asia'; case DocumentType.SCREENSHOT: return 'photo-video'; @@ -1478,16 +1559,26 @@ export namespace Doc { } } - export async function importDocument(file: File) { + /// + // imports a previously exported zip file which contains a set of documents and their assets (eg, images, videos) + // the 'remap' parameter determines whether the ids of the documents loaded should be kept as they were, or remapped to new ids + // If they are not remapped, loading the file will overwrite any existing documents with those ids + // + export async function importDocument(file: File, remap = false) { const upload = Utils.prepend('/uploadDoc'); const formData = new FormData(); if (file) { formData.append('file', file); - formData.append('remap', 'true'); + formData.append('remap', remap.toString()); const response = await fetch(upload, { method: 'POST', body: formData }); const json = await response.json(); if (json !== 'error') { - const doc = await DocServer.GetRefField(json); + const docs = await DocServer.GetRefFields(json.docids as string[]); + const doc = DocCast(await DocServer.GetRefField(json.id)); + const links = await DocServer.GetRefFields(json.linkids as string[]); + Array.from(Object.keys(links)) + .map(key => links[key]) + .forEach(link => link instanceof Doc && LinkManager.Instance.addLink(link)); return doc; } } @@ -1635,11 +1726,14 @@ export namespace Doc { } } +export function IdToDoc(id: string) { + return DocCast(DocServer.GetCachedRefField(id)); +} ScriptingGlobals.add(function idToDoc(id: string): any { - return DocServer.GetCachedRefField(id); + return IdToDoc(id); }); -ScriptingGlobals.add(function renameAlias(doc: any) { - return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.aliasNumber})`; +ScriptingGlobals.add(function renameEmbedding(doc: any) { + return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); ScriptingGlobals.add(function getProto(doc: any) { return Doc.GetProto(doc); @@ -1647,8 +1741,8 @@ ScriptingGlobals.add(function getProto(doc: any) { ScriptingGlobals.add(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); -ScriptingGlobals.add(function getAlias(doc: any) { - return Doc.MakeAlias(doc); +ScriptingGlobals.add(function getEmbedding(doc: any) { + return Doc.MakeEmbedding(doc); }); ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); @@ -1672,6 +1766,16 @@ ScriptingGlobals.add(function undo() { SelectionManager.DeselectAll(); return UndoManager.Undo(); }); + +export function ShowUndoStack() { + SelectionManager.DeselectAll(); + var buffer = ''; + UndoManager.undoStack.forEach((batch, i) => { + buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n'; + ///batch.forEach(undo => (buffer += ' ' + undo.prop + '\n')); + }); + alert(buffer); +} ScriptingGlobals.add(function redo() { SelectionManager.DeselectAll(); return UndoManager.Redo(); diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts new file mode 100644 index 000000000..65decc147 --- /dev/null +++ b/src/fields/DocSymbols.ts @@ -0,0 +1,26 @@ +export const Update = Symbol('DocUpdate'); +export const Self = Symbol('DocSelf'); +export const SelfProxy = Symbol('DocSelfProxy'); +export const FieldKeys = Symbol('DocFieldKeys'); +export const FieldTuples = Symbol('DocFieldTuples'); +export const Width = Symbol('DocWidth'); +export const Height = Symbol('DocHeight'); +export const Animation = Symbol('DocAnimation'); +export const Highlight = Symbol('DocHighlight'); +export const DocData = Symbol('DocData'); +export const DocLayout = Symbol('DocLayout'); +export const DocFields = Symbol('DocFields'); +export const DocCss = Symbol('DocCss'); +export const DocAcl = Symbol('DocAcl'); +export const DirectLinks = Symbol('DocDirectLinks'); +export const AclUnset = Symbol('DocAclUnset'); +export const AclPrivate = Symbol('DocAclOwnerOnly'); +export const AclReadonly = Symbol('DocAclReadOnly'); +export const AclAugment = Symbol('DocAclAugment'); +export const AclSelfEdit = Symbol('DocAclSelfEdit'); +export const AclEdit = Symbol('DocAclEdit'); +export const AclAdmin = Symbol('DocAclAdmin'); +export const UpdatingFromServer = Symbol('DocUpdatingFromServer'); +export const Initializing = Symbol('DocInitializing'); +export const ForceServerWrite = Symbol('DocForceServerWrite'); +export const CachedUpdates = Symbol('DocCachedUpdates'); 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..c381f14f5 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -1,12 +1,9 @@ - -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 HandleUpdate = Symbol('FieldHandleUpdate'); +export const Id = Symbol('FieldId'); +export const OnUpdate = Symbol('FieldOnUpdate'); +export const Parent = Symbol('FieldParent'); +export const Copy = Symbol('FieldCopy'); +export const ToValue = Symbol('FieldToValue'); +export const ToScriptString = Symbol('FieldToScriptString'); +export const ToPlainText = Symbol('FieldToPlainText'); +export const ToString = Symbol('FieldToString'); diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts deleted file mode 100644 index 76c4ddf1b..000000000 --- a/src/fields/IconField.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; - -@Deserializable("icon") -export class IconField extends ObjectField { - @serializable(primitive()) - readonly icon: string; - - constructor(icon: string) { - super(); - this.icon = icon; - } - - [Copy]() { - return new IconField(this.icon); - } - - [ToScriptString]() { - return "invalid"; - } - [ToString]() { - return "ICONfield"; - } -} diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index a074098c1..22bea3927 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -1,5 +1,5 @@ import { Bezier } from 'bezier-js'; -import { createSimpleSchema, list, object, serializable } from 'serializr'; +import { alias, createSimpleSchema, list, object, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; import { Copy, ToScriptString, ToString } from './FieldSymbols'; @@ -64,7 +64,8 @@ const strokeDataSchema = createSimpleSchema({ @Deserializable('ink') export class InkField extends ObjectField { - @serializable(list(object(strokeDataSchema))) + public static InkDataFieldName = '__inkData'; + @serializable(alias(InkField.InkDataFieldName, list(object(strokeDataSchema)))) readonly inkData: InkData; constructor(data: InkData) { diff --git a/src/fields/List.ts b/src/fields/List.ts index edaa16003..033fa569b 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -3,8 +3,9 @@ import { alias, list, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { FieldTuples, Self, SelfProxy, Update } from './DocSymbols'; import { Field } from './Doc'; -import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; +import { Copy, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -21,12 +22,12 @@ const listHandlers: any = { if (value instanceof RefField) { throw new Error('fill with RefFields not supported yet'); } - const res = this[Self].__fields.fill(value, start, end); + const res = this[Self].__fieldTuples.fill(value, start, end); this[Update](); return res; }, pop(): any { - const field = toRealField(this[Self].__fields.pop()); + const field = toRealField(this[Self].__fieldTuples.pop()); this[Update](); return field; }, @@ -34,7 +35,7 @@ const listHandlers: any = { items = items.map(toObjectField); const list = this[Self]; - const length = list.__fields.length; + const length = list.__fieldTuples.length; for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -43,23 +44,23 @@ const listHandlers: any = { item[OnUpdate] = updateFunction(list, i + length, item, this); } } - const res = list.__fields.push(...items); + const res = list.__fieldTuples.push(...items); this[Update]({ op: '$addToSet', items, length: length + items.length }); return res; }), reverse() { - const res = this[Self].__fields.reverse(); + const res = this[Self].__fieldTuples.reverse(); this[Update](); return res; }, shift() { - const res = toRealField(this[Self].__fields.shift()); + const res = toRealField(this[Self].__fieldTuples.shift()); this[Update](); return res; }, sort(cmpFunc: any) { this[Self].__realFields(); // coerce retrieving entire array - const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); + const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); this[Update](); return res; }, @@ -67,7 +68,7 @@ const listHandlers: any = { this[Self].__realFields(); // coerce retrieving entire array items = items.map(toObjectField); const list = this[Self]; - const removed = list.__fields.filter((item: any, i: number) => i >= start && i < start + deleteCount); + const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount); for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -77,12 +78,18 @@ const listHandlers: any = { item[OnUpdate] = updateFunction(list, i + start, item, this); } } - const res = list.__fields.splice(start, deleteCount, ...items); + let hintArray: { val: any; index: number }[] = []; + for (let i = start; i < start + deleteCount; i++) { + hintArray.push({ val: list.__fieldTuples[i], index: i }); + } + const res = list.__fieldTuples.splice(start, deleteCount, ...items); + // the hint object sends the starting index of the slice and the number + // of elements to delete. this[Update]( items.length === 0 && deleteCount - ? { op: '$remFromSet', items: removed, length: list.__fields.length } - : items.length && !deleteCount && start === list.__fields.length - ? { op: '$addToSet', items, length: list.__fields.length } + ? { op: '$remFromSet', items: removed, hint: { start: start, deleteCount: deleteCount }, length: list.__fieldTuples.length } + : items.length && !deleteCount && start === list.__fieldTuples.length + ? { op: '$addToSet', items, length: list.__fieldTuples.length } : undefined ); return res.map(toRealField); @@ -90,7 +97,7 @@ const listHandlers: any = { unshift(...items: any[]) { items = items.map(toObjectField); const list = this[Self]; - const length = list.__fields.length; + const length = list.__fieldTuples.length; for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -100,43 +107,46 @@ const listHandlers: any = { item[OnUpdate] = updateFunction(list, i, item, this); } } - const res = this[Self].__fields.unshift(...items); + const res = this[Self].__fieldTuples.unshift(...items); this[Update](); return res; }, /// Accessor methods concat: action(function (this: any, ...items: any[]) { this[Self].__realFields(); - return this[Self].__fields.map(toRealField).concat(...items); + return this[Self].__fieldTuples.map(toRealField).concat(...items); }), includes(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields().includes(valueToFind, fromIndex); } else { - return this[Self].__fields.includes(valueToFind, fromIndex); + return this[Self].__fieldTuples.includes(valueToFind, fromIndex); } }, indexOf(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields().indexOf(valueToFind, fromIndex); } else { - return this[Self].__fields.indexOf(valueToFind, fromIndex); + return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); } }, join(separator: any) { this[Self].__realFields(); - return this[Self].__fields.map(toRealField).join(separator); + return this[Self].__fieldTuples.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); } else { - return this[Self].__fields.lastIndexOf(valueToFind, fromIndex); + return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); } }, slice(begin: number, end: number) { this[Self].__realFields(); - return this[Self].__fields.slice(begin, end).map(toRealField); + return this[Self].__fieldTuples.slice(begin, end).map(toRealField); }, /// Iteration methods @@ -147,55 +157,55 @@ const listHandlers: any = { return this[Self].__realFields().every(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, filter(callback: any, thisArg: any) { return this[Self].__realFields().filter(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, find(callback: any, thisArg: any) { return this[Self].__realFields().find(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, findIndex(callback: any, thisArg: any) { return this[Self].__realFields().findIndex(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, forEach(callback: any, thisArg: any) { return this[Self].__realFields().forEach(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, map(callback: any, thisArg: any) { return this[Self].__realFields().map(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, reduce(callback: any, initialValue: any) { return this[Self].__realFields().reduce(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, reduceRight(callback: any, initialValue: any) { return this[Self].__realFields().reduceRight(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, some(callback: any, thisArg: any) { return this[Self].__realFields().some(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, values() { return this[Self].__realFields().values(); @@ -210,10 +220,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]; } @@ -237,6 +247,7 @@ type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>; type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T; +export const ListFieldName = 'fields'; @Deserializable('list') class ListImpl<T extends Field> extends ObjectField { constructor(fields?: T[]) { @@ -244,9 +255,9 @@ class ListImpl<T extends Field> extends ObjectField { const list = new Proxy<this>(this, { set: setter, get: listGetter, - ownKeys: target => Object.keys(target.__fields), + ownKeys: target => Object.keys(target.__fieldTuples), getOwnPropertyDescriptor: (target, prop) => { - if (prop in target.__fields) { + if (prop in target[FieldTuples]) { return { configurable: true, //TODO Should configurable be true? enumerable: true, @@ -271,36 +282,28 @@ 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[FieldTuples].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( - action(pfields => { - for (let i = 0; i < promised.length; i++) { - promised[i].field.setValue(pfields[promised[i].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); + return this[FieldTuples].map(toRealField); } - @serializable(alias('fields', list(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fields() { - return this.___fields; + @serializable(alias(ListFieldName, list(autoObject(), { afterDeserialize: afterDocDeserialize }))) + private get __fieldTuples() { + return this[FieldTuples]; } - private set __fields(value) { - this.___fields = value; + private set __fieldTuples(value) { + this[FieldTuples] = value; for (const key in value) { const field = value[key]; if (field instanceof ObjectField) { @@ -311,14 +314,14 @@ class ListImpl<T extends Field> extends ObjectField { } [Copy]() { - const copiedData = this[Self].__fields.map(f => (f instanceof ObjectField ? f[Copy]() : f)); + const copiedData = this[Self].__fieldTuples.map(f => (f instanceof ObjectField ? f[Copy]() : f)); const deepCopy = new ListImpl<T>(copiedData as any); return deepCopy; } // @serializable(alias("fields", list(autoObject()))) @observable - private ___fields: StoredType<T>[] = []; + private [FieldTuples]: StoredType<T>[] = []; private [Update] = (diff: any) => { // console.log(diff); diff --git a/src/fields/ListSpec.ts b/src/fields/ListSpec.ts deleted file mode 100644 index e69de29bb..000000000 --- a/src/fields/ListSpec.ts +++ /dev/null diff --git a/src/fields/PresField.ts b/src/fields/PresField.ts deleted file mode 100644 index f236a04fd..000000000 --- a/src/fields/PresField.ts +++ /dev/null @@ -1,6 +0,0 @@ -//insert code here -import { ObjectField } from "./ObjectField"; - -export abstract class PresField extends ObjectField { - -}
\ No newline at end of file diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index e924ef7a3..c076f5fe1 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -1,17 +1,16 @@ import { Deserializable } from '../client/util/SerializationHelper'; -import { FieldWaiting } from './Doc'; +import { Field, FieldWaiting, Opt } from './Doc'; import { primitive, serializable } from 'serializr'; -import { observable, action, runInAction } from 'mobx'; +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 } from './FieldSymbols'; +import { Id, Copy, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; -import { Plugins } from './util'; 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) @@ -25,18 +24,22 @@ export class ProxyField<T extends RefField> extends ObjectField { //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 Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? } [ToString]() { return 'ProxyField'; @@ -48,48 +51,40 @@ export class ProxyField<T extends RefField> extends ObjectField { // 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>; - @action - value(): T | undefined | FieldWaiting<T> { - if (this.cache) return this.cache; + @computed get value(): T | undefined | FieldWaiting<T> { + if (this.cache.field) return this.cache.field; if (this.failed) return undefined; - const cached = DocServer.GetCachedRefField(this.fieldId) as T; - if (cached !== undefined) { - this.cache = cached; - } else if (!this.promise) { - this.promise = DocServer.GetRefField(this.fieldId).then( - action((field: any) => { - this.promise = undefined; - this.cache = field; - this.failed = field === undefined; - return field; - }) - ) as FieldWaiting<T>; + 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 cached ?? this.promise; + return this.cache.field ?? this.cache.p; } - promisedValue(): string { - return !this.cache && !this.failed && !this.promise && !DocServer.GetCachedRefField(this.fieldId) ? this.fieldId : ''; + @computed get needsRequesting(): boolean { + return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false; } - setPromise(promise: any) { - this.promise = promise; + + 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,17 +108,15 @@ 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 diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index d7edd4266..3e75a071f 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -1,11 +1,11 @@ -import { serializable } from "serializr"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { Deserializable } from "../client/util/SerializationHelper"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { ObjectField } from "./ObjectField"; +import { serializable } from 'serializr'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { Deserializable } from '../client/util/SerializationHelper'; +import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { ObjectField } from './ObjectField'; @scriptingGlobal -@Deserializable("RichTextField") +@Deserializable('RichTextField') export class RichTextField extends ObjectField { @serializable(true) readonly Data: string; @@ -13,14 +13,14 @@ export class RichTextField extends ObjectField { @serializable(true) readonly Text: string; - constructor(data: string, text: string = "") { + constructor(data: string, text: string = '') { super(); this.Data = data; this.Text = text; } Empty() { - return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align")); + return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align')); } [Copy]() { @@ -28,14 +28,16 @@ export class RichTextField extends ObjectField { } [ToScriptString]() { - return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`; + return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`; } [ToString]() { return this.Text; } public static DashField(fieldKey: string) { - return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, ""); + return new RichTextField( + `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docId":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, + '' + ); } - -}
\ No newline at end of file +} diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index bf055cd8b..24cd078f2 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -264,18 +264,18 @@ export namespace RichTextUtils { const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => { const { url: src, width, agnostic } = image; - let docid: string; + let docId: string; const guid = Utils.GenerateDeterministicGuid(agnostic); const backingDocId = StrCast(textNote[guid]); if (!backingDocId) { const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 }); DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument); - docid = backingDoc[Id]; - textNote[guid] = docid; + docId = backingDoc[Id]; + textNote[guid] = docId; } else { - docid = backingDocId; + docId = backingDocId; } - return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' }); + return schema.node('image', { src, agnostic, width, docId, float: null, location: 'add:right' }); }; const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { @@ -395,11 +395,11 @@ export namespace RichTextUtils { if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]); if (linkDoc instanceof Doc) { - let exported = (await Cast(linkDoc.anchor2, Doc))!; + let exported = (await Cast(linkDoc.link_anchor_2, Doc))!; if (!exported.customLayout) { - exported = Doc.MakeAlias(exported); + exported = Doc.MakeEmbedding(exported); DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument); - linkDoc.anchor2 = exported; + linkDoc.link_anchor_2 = exported; } url = Utils.shareUrl(exported[Id]); } diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 7ad376a28..f035eeb0d 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -1,13 +1,13 @@ -import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types"; -import { Doc, Field } from "./Doc"; -import { ObjectField } from "./ObjectField"; -import { RefField } from "./RefField"; -import { SelfProxy } from "./FieldSymbols"; -import { List } from "./List"; +import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types'; +import { Doc, Field } from './Doc'; +import { ObjectField } from './ObjectField'; +import { RefField } from './RefField'; +import { SelfProxy } from './DocSymbols'; +import { List } from './List'; type AllToInterface<T extends Interface[]> = { - 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>, - 0: ToInterface<Head<T>> + 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>; + 0: ToInterface<Head<T>>; }[HasTail<T> extends true ? 1 : 0]; export const emptySchema = createSchema({}); @@ -28,36 +28,40 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu schema[key] = s[key]; } } - const proto = new Proxy({}, { - get(target: any, prop, receiver) { - 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 - return Cast(field, desc.type, desc.defaultVal); - } - if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { - const doc = Cast(field, Doc); - if (doc === undefined) { - return undefined; + const proto = new Proxy( + {}, + { + get(target: any, prop, receiver) { + 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 + return Cast(field, desc.type, desc.defaultVal); } - if (doc instanceof Doc) { - return desc(doc); + if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { + const doc = Cast(field, Doc); + if (doc === undefined) { + return undefined; + } + if (doc instanceof Doc) { + return desc(doc); + } + return doc.then(doc => doc && desc(doc)); } - return doc.then(doc => doc && desc(doc)); + return Cast(field, desc); } - return Cast(field, desc); - } - return field; - }, - set(target: any, prop, value, receiver) { - receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls - return true; + return field; + }, + set(target: any, prop, value, receiver) { + receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls + return true; + }, } - }); + ); // !(doc instanceof Doc) && (throw new Error("Currently wrapping a schema in another schema isn't supported")); const fn = (doc: Doc) => Object.create(proto, { doc: { value: doc[SelfProxy], writable: false } }); - return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc)) as InterfaceFunc<T>; + return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc())) as InterfaceFunc<T>; } export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>; @@ -75,8 +79,8 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) this.__doc[key] = value; return; } - throw new TypeError("Expected type " + type); - } + throw new TypeError('Expected type ' + type); + }, }); } return function (doc: any) { @@ -95,12 +99,12 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon } export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> { - return { List: type as any };//TODO Types + return { List: type as any }; //TODO Types } export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> { return { type: type as any, - defaultVal + defaultVal, }; -}
\ No newline at end of file +} diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 0b51db70b..4b1855cb0 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -2,7 +2,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, primitive } from 'serializr'; import { ObjectField } from './ObjectField'; import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols'; -import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; export const PastelSchemaPalette = new Map<string, string>([ @@ -115,9 +115,12 @@ export class SchemaHeaderField extends ObjectField { } [ToScriptString]() { - return `header(${this.heading},${this.type},${this.width}})`; + return `schemaHeaderField("${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed})`; } [ToString]() { return `SchemaHeaderField`; } } +ScriptingGlobals.add(function schemaHeaderField(heading: string, color: string, type: number, width: number, desc?: boolean, collapsed?: boolean) { + return new SchemaHeaderField(heading, color, type, width, desc, collapsed); +}); diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 4896c027d..2b8750714 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( @@ -104,30 +103,9 @@ export class ScriptField extends ObjectField { } this.rawscript = rawscript; this.setterscript = setterscript; - this.script = script ?? (CompileScript('false', { addReturn: true }) as CompiledScript); + this.script = script ?? ScriptField.GetScriptFieldCache('false:') ?? (CompileScript('false', { addReturn: true }) as CompiledScript); } - // init(callback: (res: Field) => any) { - // const options = this.options!; - // const keys = Object.keys(options.options.capturedIds); - // Server.GetFields(keys).then(fields => { - // let captured: { [name: string]: Field } = {}; - // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]); - // const opts: ScriptOptions = { - // addReturn: options.options.addReturn, - // params: options.options.params, - // requiredType: options.options.requiredType, - // capturedVariables: captured - // }; - // const script = CompileScript(options.script, opts); - // if (!script.compiled) { - // throw new Error("Can't compile script"); - // } - // this._script = script; - // callback(this); - // }); - // } - [Copy](): ObjectField { return new ScriptField(this.script, this.setterscript, this.rawscript); } @@ -136,7 +114,7 @@ export class ScriptField extends ObjectField { } [ToScriptString]() { - return 'script field'; + return this.script.originalScript; } [ToString]() { return this.script.originalScript; @@ -173,19 +151,20 @@ export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc 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); + _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) ?? doc, value: '', _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); } - public static MakeScript(script: string, params: object = {}) { - const compiled = ScriptField.CompileScript(script, params, false); - return compiled.compiled ? new ComputedField(compiled) : undefined; - } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { - const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); - return compiled.compiled ? new ComputedField(compiled) : undefined; + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) { + const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables }); + const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined; + const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined; + return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined; } public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) { if (!doc[`${fieldKey}-indexed`]) { @@ -208,14 +187,20 @@ export class ComputedField extends ScriptField { return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + if (doc[`${fieldKey}`] instanceof List) return; if (!doc[`${fieldKey}-indexed`]) { const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]); flist[curTimecode] = Field.Copy(doc[fieldKey]); doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); - const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); - return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + const setField = ScriptField.CompileScript( + `{setIndexVal (self['${fieldKey}-indexed'], self.${interpolatorKey}, value); console.log(self["${fieldKey}-indexed"][self.${interpolatorKey}],self.data,self["${fieldKey}-indexed"]))}`, + { value: 'any' }, + false, + {} + ); + return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined); } } export namespace ComputedField { @@ -239,12 +224,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/Types.ts b/src/fields/Types.ts index bf40a0d7b..251b1149d 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -1,48 +1,50 @@ -import { Field, Opt, FieldResult, Doc } from "./Doc"; -import { List } from "./List"; -import { RefField } from "./RefField"; -import { DateField } from "./DateField"; -import { ScriptField } from "./ScriptField"; -import { URLField, WebField, ImageField } from "./URLField"; -import { TextField } from "@material-ui/core"; -import { RichTextField } from "./RichTextField"; - -export type ToType<T extends InterfaceValue> = - T extends "string" ? string : - T extends "number" ? number : - T extends "boolean" ? boolean : - T extends ListSpec<infer U> ? List<U> : - // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never; - T extends DefaultFieldConstructor<infer _U> ? never : - T extends { new(...args: any[]): List<Field> } ? never : - T extends { new(...args: any[]): infer R } ? R : - T extends (doc?: Doc) => infer R ? R : never; - -export type ToConstructor<T extends Field> = - T extends string ? "string" : - T extends number ? "number" : - T extends boolean ? "boolean" : - T extends List<infer U> ? ListSpec<U> : - new (...args: any[]) => T; +import { Field, Opt, FieldResult, Doc } from './Doc'; +import { List } from './List'; +import { RefField } from './RefField'; +import { DateField } from './DateField'; +import { ScriptField } from './ScriptField'; +import { URLField, WebField, ImageField, CsvField } from './URLField'; +import { TextField } from '@material-ui/core'; +import { RichTextField } from './RichTextField'; + +export type ToType<T extends InterfaceValue> = T extends 'string' + ? string + : T extends 'number' + ? number + : T extends 'boolean' + ? boolean + : T extends ListSpec<infer U> + ? List<U> + : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never; + T extends DefaultFieldConstructor<infer _U> + ? never + : T extends { new (...args: any[]): List<Field> } + ? never + : T extends { new (...args: any[]): infer R } + ? R + : T extends (doc?: Doc) => infer R + ? R + : never; + +export type ToConstructor<T extends Field> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T; export type ToInterface<T extends Interface> = { - [P in Exclude<keyof T, "proto">]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>; + [P in Exclude<keyof T, 'proto'>]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>; }; // type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> }; export type ListSpec<T extends Field> = { List: ToConstructor<T> }; export type DefaultFieldConstructor<T extends Field> = { - type: ToConstructor<T>, - defaultVal: T + type: ToConstructor<T>; + defaultVal: T; }; // type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1]; export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never; -export type Tail<T extends any[]> = - ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []; -export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true; +export type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer TT) => any ? TT : []; +export type HasTail<T extends any[]> = T extends [] | [any] ? false : true; export type InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any); //TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial @@ -58,14 +60,14 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResu export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined { if (field instanceof Promise) { - return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal; + return defaultVal === undefined ? (field.then(f => Cast(f, ctor) as any) as any) : defaultVal === null ? undefined : defaultVal; } if (field !== undefined && !(field instanceof Promise)) { - if (typeof ctor === "string") { + if (typeof ctor === 'string') { if (typeof field === ctor) { return field as ToType<T>; } - } else if (typeof ctor === "object") { + } else if (typeof ctor === 'object') { if (field instanceof List) { return field as any; } @@ -77,19 +79,20 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal } export function DocCast(field: FieldResult, defaultVal?: Doc) { - return Cast(field, Doc, null) ?? defaultVal; + const doc = Cast(field, Doc, null); + return doc && !(doc instanceof Promise) ? doc : (defaultVal as Doc); } export function NumCast(field: FieldResult, defaultVal: number | null = 0) { - return Cast(field, "number", defaultVal); + return Cast(field, 'number', defaultVal); } -export function StrCast(field: FieldResult, defaultVal: string | null = "") { - return Cast(field, "string", defaultVal); +export function StrCast(field: FieldResult, defaultVal: string | null = '') { + return Cast(field, 'string', defaultVal); } export function BoolCast(field: FieldResult, defaultVal: boolean | null = false) { - return Cast(field, "boolean", defaultVal); + return Cast(field, 'boolean', defaultVal); } export function DateCast(field: FieldResult) { return Cast(field, DateField, null); @@ -101,6 +104,9 @@ export function RTFCast(field: FieldResult) { export function ScriptCast(field: FieldResult, defaultVal: ScriptField | null = null) { return Cast(field, ScriptField, defaultVal); } +export function CsvCast(field: FieldResult, defaultVal: CsvField | null = null) { + return Cast(field, CsvField, defaultVal); +} export function WebCast(field: FieldResult, defaultVal: WebField | null = null) { return Cast(field, WebField, defaultVal); } @@ -113,7 +119,7 @@ type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefFiel export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>; export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>; export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> { - return (field instanceof Promise || field === undefined) ? defaultValue : field; + return field instanceof Promise || field === undefined ? defaultValue : field; } export interface PromiseLike<T> { @@ -121,5 +127,9 @@ export interface PromiseLike<T> { } export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> { if (field instanceof Promise) return field as Promise<Opt<T>>; - return { then(cb: ((field: Opt<T>) => void)) { return cb(field); } }; -}
\ No newline at end of file + return { + then(cb: (field: Opt<T>) => void) { + return cb(field); + }, + }; +} diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 00c78e231..8ac20b1e5 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -54,9 +54,6 @@ export const nullAudio = 'https://actions.google.com/sounds/v1/alarms/beep_short @Deserializable('audio') export class AudioField extends URLField {} @scriptingGlobal -@Deserializable('recording') -export class RecordingField extends URLField {} -@scriptingGlobal @Deserializable('image') export class ImageField extends URLField {} @scriptingGlobal @@ -69,14 +66,8 @@ export class PdfField extends URLField {} @Deserializable('web') export class WebField extends URLField {} @scriptingGlobal -@Deserializable('map') -export class MapField extends URLField {} -@scriptingGlobal @Deserializable('csv') export class CsvField extends URLField {} @scriptingGlobal @Deserializable('youtube') export class YoutubeField extends URLField {} -@scriptingGlobal -@Deserializable('webcam') -export class WebCamField extends URLField {} diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 24b5a359d..76b287be7 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -9,15 +9,15 @@ export const documentSchema = createSchema({ type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_') title: 'string', // document title (can be on either data document or layout) isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field - creationDate: DateField, // when the document was created + author_date: DateField, // when the document was created links: listSpec(Doc), // computed (readonly) list of links associated with this document // "Location" properties in a very general sense - _curPage: 'number', // current page of a page based document + _layout_curPage: 'number', // current page of a page based document _currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide) lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide) activeFrame: 'number', // the active frame of a frame based animated document - _currentTimecode: 'number', // current play back time of a temporal document (video / audio) + _layout_currentTimecode: 'number', // current play back time of a temporal document (video / audio) _timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) _timecodeToHIde: 'number', // the time that a document should be hidden markers: listSpec(Doc), // list of markers for audio / video @@ -25,52 +25,52 @@ export const documentSchema = createSchema({ y: 'number', // y coordinate when in a freeform view z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview zIndex: 'number', // zIndex of a document in a freeform view - _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web) + _layout_scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web) lat: 'number', lng: 'number', // appearance properties on the layout '_backgroundGrid-spacing': 'number', // the size of the grid for collection views - _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents + _layout_autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents _nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set _nativeHeight: 'number', // " _width: 'number', // width of document in its container's coordinate system _height: 'number', // " - _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set - _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set + _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when freeform_fitContentsToBox is set + _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when freeform_fitContentsToBox is set _xMargin: 'number', // margin added on left/right of most documents to add separation from their container _yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container _overflow: 'string', // sets overflow behvavior for CollectionFreeForm views - _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document - _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover + _layout_showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document + _layout_showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover _pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis _columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column _columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none) _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: 'string', - _fontFamily: 'string', - _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar + // _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry + // _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views + _text_fontSize: 'string', + _text_fontFamily: 'string', + _layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar // appearance properties on the data document backgroundColor: 'string', // background color of document - borderRounding: 'string', // border radius rounding of document - boxShadow: 'string', // the amount of shadow around the perimeter of a document + layout_borderRounding: 'string', // border radius rounding of document + layout_boxShadow: 'string', // the amount of shadow around the perimeter of a document color: 'string', // foreground color of document - fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box + freeform_fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box 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 + stroke_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 layout_fieldKey below + layout_fieldKey: 'string', // holds the field key for the field that actually holds the current lyoat letterSpacing: 'string', opacity: 'number', // opacity of document - strokeWidth: 'number', - strokeBezier: 'number', - strokeStartMarker: 'string', - strokeEndMarker: 'string', - strokeDash: 'string', + stroke_width: 'number', + stroke_bezier: 'number', + stroke_startMarker: 'string', + stroke_endMarker: 'string', + stroke_dash: '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 @@ -84,24 +84,23 @@ export const documentSchema = createSchema({ onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) 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., add:right, inPlace, default, ) + followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, ) 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 + layout_hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden + link_displayLine: 'boolean', // whether a link connection should be shown between link anchor endpoints. + isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations 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 + link_displayArrow: 'boolean', // toggles directed arrows // drag drop properties - _stayInCollection: 'boolean', // whether document can be dropped into a different collection + _dragOnlyWithinContainer: 'boolean', // whether document can be dropped into a different collection dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. - dropAction: 'string', // override specifying what should happen when this document is dropped (can be "alias", "copy", "move") - targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move' - childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy") - removeDropProperties: listSpec('string'), // properties that should be removed from the alias/copy/etc of this document when it is dropped + dropAction: 'string', // override specifying what should happen when something is dropped on this document (can be "embed", "copy", "move") + dragAction: 'string', // override specifying what should happen when this document s dragged (can be "embed", "copy", "move") + childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy") + dropPropertiesToRemove: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped }); export const collectionSchema = createSchema({ diff --git a/src/fields/util.ts b/src/fields/util.ts index 4a62a6a1f..e439768ee 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,42 +1,22 @@ -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 { LinkManager } from '../client/util/LinkManager'; 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, - AclPrivate, - AclReadonly, - AclSelfEdit, - AclSym, - AclUnset, - DataSym, - Doc, - DocListCast, - DocListCastAsync, - FieldResult, - ForceServerWrite, - HeightSym, - Initializing, - LayoutSym, - updateCachedAcls, - UpdatingFromServer, - WidthSym, -} from './Doc'; -import { Id, OnUpdate, Parent, Self, SelfProxy, Update } from './FieldSymbols'; +import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc'; +import { AclAdmin, AclEdit, AclPrivate, AclSelfEdit, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols'; +import { Id, OnUpdate, Parent, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { RefField } from './RefField'; import { RichTextField } from './RichTextField'; import { SchemaHeaderField } from './SchemaHeaderField'; -import { ComputedField } from './ScriptField'; +import { ComputedField, ScriptField } from './ScriptField'; import { ScriptCast, StrCast } from './Types'; function _readOnlySetter(): never { @@ -48,33 +28,16 @@ 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()) { + if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') { target[prop] = value; return true; } - if (typeof prop === 'symbol') { - target[prop] = value; - return true; - } if (value !== undefined) { value = value[SelfProxy] || value; } - const curValue = target.__fields[prop]; + const curValue = target.__fieldTuples[prop]; if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way @@ -83,6 +46,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (value instanceof RefField) { value = new ProxyField(value); } + if (value instanceof ObjectField) { if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) { throw new Error("Can't put the same object in multiple documents at the same time"); @@ -105,30 +69,41 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (writeToDoc) { if (value === undefined) { - target.__fieldKeys && delete target.__fieldKeys[prop]; - delete target.__fields[prop]; + target[FieldKeys] && delete target[FieldKeys][prop]; // Lists don't have a FieldKeys field + delete target.__fieldTuples[prop]; } else { - target.__fieldKeys && (target.__fieldKeys[prop] = true); - // if (target.__fields[prop] !== value) { - // console.log("ASSIGN " + prop + " " + value); - // } - target.__fields[prop] = value; + // bcz: uncomment to see if server is being updated + // console.log(prop + ' = ' + value + '(' + curValue + ')'); + target[FieldKeys] && (target[FieldKeys][prop] = true); // Lists don't have a FieldKeys field + target.__fieldTuples[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 } }); + if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } !receiver[Initializing] && + !StrListCast(receiver.undoIgnoreFields).includes(prop.toString()) && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && - UndoManager.AddEvent({ - redo: () => (receiver[prop] = value), - undo: () => (receiver[prop] = curValue), - prop: prop?.toString(), - }); + UndoManager.AddEvent( + { + redo: () => (receiver[prop] = value), + undo: () => { + const wasUpdate = receiver[UpdatingFromServer]; + const wasForce = receiver[ForceServerWrite]; + receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet + 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[ForceServerWrite] = wasForce; + receiver[UpdatingFromServer] = wasUpdate; + }, + prop: prop?.toString(), + }, + value + ); return true; } return false; @@ -151,24 +126,17 @@ export function denormalizeEmail(email: string) { return email.replace(/__/g, '.'); } -// playground mode allows the user to add/delete documents or make layout changes without them saving to the server -// let playgroundMode = false; - -// export function togglePlaygroundMode() { -// playgroundMode = !playgroundMode; -// } - /** * Copies parent's acl fields to the child */ export function inheritParentAcls(parent: Doc, child: Doc) { return; - const dataDoc = parent[DataSym]; - for (const key of Object.keys(dataDoc)) { - // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. - const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key]; - key.startsWith('acl') && distributeAcls(key, permission, child); - } + // const dataDoc = parent[DataSym]; + // for (const key of Object.keys(dataDoc)) { + // // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. + // const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key]; + // key.startsWith('acl') && distributeAcls(key, permission, child); + // } } /** @@ -183,8 +151,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 +175,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); @@ -230,43 +195,33 @@ export function SetCachedGroups(groups: string[]) { runInAction(() => cachedGroups.push(...groups)); } 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; + const targetAcls = target[DocAcl]; + 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.__fieldTuples?.author || target.author; // target may be a Doc of Proxy, so check __fieldTuples.author and .author + if (targetAuthor && targetAuthor !== userChecked) return AclPrivate; return AclAdmin; } /** @@ -279,9 +234,9 @@ function getEffectiveAcl(target: any, user?: string): symbol { */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) { if (!visited) visited = [] as Doc[]; - if (visited.includes(target)) return; + if (!target || visited.includes(target)) return; - if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { + if ((target._type_collection === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { target[key] = acl; if (target !== Doc.GetProto(target)) { //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???) @@ -291,21 +246,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,34 +259,27 @@ 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[DocData]; + 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)); + LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - // } - const data = d[DataSym]; - if (data) { - distributeAcls(key, acl, data, inheritingFromCollection, visited); - } + distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); }); // maps over the annotations of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - // } - const data = d[DataSym]; - if (data) { - distributeAcls(key, acl, data, inheritingFromCollection, visited); - } + distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); }); } @@ -353,80 +289,71 @@ 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") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].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 !== '__id' && prop !== '__fields' && prop.startsWith('_')) { + if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { if (!prop.startsWith('__')) prop = prop.substring(1); if (target.__LAYOUT__) { target.__LAYOUT__[prop] = value; return true; } } - if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { - return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + if (target.__fieldTuples[prop] instanceof ComputedField) { + if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { + return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + } } 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]; +export function getter(target: any, prop: string | symbol, proxy: any): any { + // prettier-ignore + switch (prop) { + case 'then' : return undefined; + case 'constructor': case 'toString': case 'valueOf': + case 'serializeInfo': case 'factory': + return target[prop]; + case DocAcl : return target[DocAcl]; + case $mobx: return target.__fieldTuples[prop]; + case DocLayout: return target.__LAYOUT__; + case Height: case Width: 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 (prop === 'then') { - //If we're being awaited - 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.__fieldTuples[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) { if (typeof prop === 'symbol') { delete target[prop]; - return true; + } else { + target[SelfProxy][prop] = undefined; } - target[SelfProxy][prop] = undefined; return true; } @@ -437,7 +364,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === '$addToSet' ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } : diff?.op === '$remFromSet' - ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)), hint: diff.hint } } : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } }; !op.$set && ((op as any).length = diff.length); const prevValue = ObjectField.MakeCopy(lastValue as List<any>); @@ -450,7 +377,8 @@ 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))); + console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo + receiver[prop].push(...diff.items.map((item: any) => item.value ?? item)); lastValue = ObjectField.MakeCopy(receiver[prop]); }, undo: action(() => { @@ -460,19 +388,20 @@ 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); } }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), - prop: '', + prop: 'add ' + diff.items.length + ' items to list', } : diff?.op === '$remFromSet' ? { redo: action(() => { + console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo 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,16 +413,17 @@ 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]); }, - prop: '', + prop: 'remove ' + diff.items.length + ' items from list', } : { redo: () => { + console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); lastValue = ObjectField.MakeCopy(receiver[prop]); }, @@ -502,8 +432,9 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); lastValue = ObjectField.MakeCopy(receiver[prop]); }, - prop: '', - } + prop: 'assign list', + }, + diff?.items ); } target[Update](op); |
