diff options
Diffstat (limited to 'src/fields')
-rw-r--r-- | src/fields/DateField.ts | 4 | ||||
-rw-r--r-- | src/fields/Doc.ts | 607 | ||||
-rw-r--r-- | src/fields/List.ts | 6 | ||||
-rw-r--r-- | src/fields/Proxy.ts | 64 | ||||
-rw-r--r-- | src/fields/RichTextField.ts | 29 | ||||
-rw-r--r-- | src/fields/RichTextUtils.ts | 3 | ||||
-rw-r--r-- | src/fields/Schema.ts | 21 | ||||
-rw-r--r-- | src/fields/ScriptField.ts | 15 | ||||
-rw-r--r-- | src/fields/Types.ts | 104 | ||||
-rw-r--r-- | src/fields/URLField.ts | 3 | ||||
-rw-r--r-- | src/fields/documentSchemas.ts | 2 | ||||
-rw-r--r-- | src/fields/util.ts | 33 |
12 files changed, 454 insertions, 437 deletions
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index f0a851ce6..5db201c72 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -39,6 +39,6 @@ export class DateField extends ObjectField { } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function d(...dateArgs: any[]) { - return new DateField(new (Date as any)(...dateArgs)); +ScriptingGlobals.add(function d(...dateArgs: ConstructorParameters<typeof Date>) { + return new DateField(new Date(...dateArgs)); }); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index fc89dcbe7..0d11b9743 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -17,7 +17,7 @@ import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScr import { InkEraserTool, InkInkTool, InkTool } from './InkField'; import { List } from './List'; import { ObjectField, serverOpType } from './ObjectField'; -import { PrefetchProxy, ProxyField } from './Proxy'; +import { PrefetchProxy } from './Proxy'; import { FieldId, RefField } from './RefField'; import { RichTextField } from './RichTextField'; import { listSpec } from './Schema'; @@ -25,6 +25,7 @@ import { ComputedField, ScriptField } from './ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types'; import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util'; import { gptImageLabel } from '../client/apis/gpt/GPT'; +import { DateField } from './DateField'; export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>; export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>; @@ -43,11 +44,11 @@ export namespace Field { * @param doc doc containing key * @param key field key to display * @param showComputedValue whether copmuted function should display its value instead of its function + * @param schemaCell * @returns string representation of the field */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean, schemaCell?: boolean): string { - const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const cfield = ComputedField.DisableCompute(() => FieldValue(doc[key])); const valFunc = (field: FieldType): string => { const res = field instanceof ComputedField && showComputedValue @@ -64,7 +65,9 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield); + const notOnTemplate = !key.startsWith('_') || doc[DocLayout] === doc; + const isOnDelegate = notOnTemplate && !Doc.IsDataProto(doc) && ((key.startsWith('_') && !Field.IsField(cfield)) || Object.keys(doc).includes(key.replace(/^_/, ''))); + return (isOnDelegate ? '=' : '') + (!Field.IsField(cfield) ? '' : valFunc(cfield)); } export function toScriptString(field: FieldType, schemaCell?: boolean) { switch (typeof field) { @@ -131,13 +134,13 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); } export function NumListCast(field: FieldResult, defaultVal: number[] = []) { - return Cast(field, listSpec('number'), defaultVal); + return Cast(field, listSpec('number'), defaultVal)!; } export function StrListCast(field: FieldResult, defaultVal: string[] = []) { - return Cast(field, listSpec('string'), defaultVal); + return Cast(field, listSpec('string'), defaultVal)!; } export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) { - return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[]; + return Cast(field, listSpec(Doc), defaultVal)!.filter(d => d instanceof Doc); } export enum aclLevel { @@ -174,13 +177,27 @@ export function updateCachedAcls(doc: Doc) { } if (doc.proto instanceof Promise) { - doc.proto.then(proto => updateCachedAcls(DocCast(proto))); + doc.proto.then(proto => DocCast(proto) && updateCachedAcls(DocCast(proto)!)); return doc.proto; } } return undefined; } +/** + * computes a field name for where to store and expanded template Doc + * The format is layout_[ROOT_TEMPLATE_NMAE]_[ROOT_TEMPLATE_CHILD_NAME]_... + * @param template the template (either a root or a root child Doc) + * @param layoutFieldKey the fieldKey of the container of the template + * @returns field key to store expanded template Doc + */ +export function expandedFieldName(template: Doc, layoutFieldKey?: string) { + const layout_key = !layoutFieldKey?.endsWith(']') + ? 'layout' // layout_SOMETHING = SOMETHING => layout_[SOMETHING] = [SOMETHING] + : layoutFieldKey; // prettier-ignore + const tempTitle = '[' + StrCast(template.title).replace(/^\[(.*)\]$/, '$1') + ']'; + return `${layout_key}_${tempTitle}`; // prettier-ignore +} @scriptingGlobal @Deserializable('Doc', (obj: unknown) => updateCachedAcls(obj as Doc), ['id']) export class Doc extends RefField { @@ -215,7 +232,7 @@ export class Doc extends RefField { public static AddLink: (link: Doc, checkExists?: boolean) => void; public static DeleteLink: (link: Doc) => void; public static Links: (link: Doc | undefined) => Doc[]; - public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined; + public static getOppositeAnchor: (linkDoc: Doc | undefined, anchor: Doc | undefined) => Doc | undefined; // KeyValueBox SetField (defined there) public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean; // UserDoc "API" @@ -237,7 +254,7 @@ export class Doc extends RefField { public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore - public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore + public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore public static get MyLightboxDrawings() { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore @@ -264,22 +281,28 @@ export class Doc extends RefField { public static set ActiveDashboard(val: Opt<Doc>) { Doc.UserDoc().activeDashboard = val; } // prettier-ignore public static get MyFilterHotKeys() { return DocListCast(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)?.data).filter(key => key.toolType !== "-opts-"); } // prettier-ignore public static RemFromFilterHotKeys(doc: Doc) { - return Doc.RemoveDocFromList(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter), 'data', doc); + return (filters => filters && Doc.RemoveDocFromList(filters, 'data', doc))(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)); } public static AddToFilterHotKeys(doc: Doc) { - return Doc.AddDocToList(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter), 'data', doc); + return (btns => btns && Doc.AddDocToList(btns, 'data', doc))(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)); } public static IsInMyOverlay(doc: Doc) { return Doc.MyOverlayDocs.includes(doc); } // prettier-ignore - public static AddToMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore - public static RemFromMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore - public static AddToMyPublished(doc: Doc) { - doc[DocData].title_custom = true; - doc[DocData].layout_showTitle = 'title'; - Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore - public static RemFromMyPublished(doc: Doc){ - doc[DocData].title_custom = false; - doc[DocData].layout_showTitle = undefined; - Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myPublishedDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore + public static AddToMyOverlay(doc: Doc) { + return Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc); + } // : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore + public static RemFromMyOverlay(doc: Doc) { + return Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard, 'myOverlayDocs', doc); + } // : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore + public static AddToMyPublished(doc: Doc) { + doc.$title_custom = true; + doc.$layout_showTitle = 'title'; + Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc); + } // : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore + public static RemFromMyPublished(doc: Doc) { + doc.$title_custom = false; + doc.$layout_showTitle = undefined; + Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard, 'myPublishedDocs', doc); + } // : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore public static IsComicStyle(doc?: Doc) { return doc && Doc.ActiveDashboard && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic' ; } // prettier-ignore constructor(id?: FieldId, forceSave?: boolean) { @@ -320,10 +343,11 @@ export class Doc extends RefField { UpdatingFromServer, Width, '__LAYOUT__', + '__DATA__', ]; }, getOwnPropertyDescriptor: (target, prop) => { - if (prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) { + if (prop.toString() === '__DATA__' || prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) { return Reflect.getOwnPropertyDescriptor(target, prop); } return { @@ -400,23 +424,23 @@ export class Doc extends RefField { public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } // prettier-ignore public get [DocData](): Doc { + return this[SelfProxy].__DATA__; + } + @computed get __DATA__(): Doc { const self = this[SelfProxy]; - return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); + return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(DocCast(self[DocLayout].rootDocument, self)!); } - @computed get __LAYOUT__(): Doc | undefined { + @computed get __LAYOUT__(): Doc { const self = this[SelfProxy]; const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null); if (templateLayoutDoc) { - let renderFieldKey: string = ''; const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; - if (typeof layoutField === 'string') { - [renderFieldKey] = layoutField.split("fieldKey={'")[1].split("'"); // layoutField.split("'")[1]; - } else { - return Cast(layoutField, Doc, null); + if (typeof layoutField !== 'string') { + return DocCast(layoutField, self)!; } - return Cast(self[renderFieldKey + '_layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc; + return DocCast(self[expandedFieldName(templateLayoutDoc)], templateLayoutDoc)!; } - return undefined; + return self; } public async [HandleUpdate](diff: { $set: { [key: string]: FieldType } } | { $unset?: unknown }) { @@ -591,7 +615,7 @@ export namespace Doc { // 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 data doc ('isDataDoc'). export function GetProto(doc: Doc): Doc { - const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : DocCast(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 { @@ -624,8 +648,8 @@ export namespace Doc { * @returns true if successful, false otherwise. */ export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, ignoreProto = false) { - const key = fieldKey || Doc.LayoutFieldKey(listDoc); - const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); + const key = fieldKey || Doc.LayoutDataKey(listDoc); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc['$' + key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { const ind = list.indexOf(doc); if (ind !== -1) { @@ -641,8 +665,8 @@ export namespace Doc { * @returns true if successful, false otherwise. */ export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean, ignoreProto?: boolean) { - const key = fieldKey || Doc.LayoutFieldKey(listDoc); - const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); + const key = fieldKey || Doc.LayoutDataKey(listDoc); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc['$' + key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { if (!allowDuplicates) { const pind = list.findIndex(d => d instanceof Doc && d[Id] === doc[Id]); @@ -680,41 +704,36 @@ export namespace Doc { return DocListCast(Doc.Get(doc[DocData], 'proto_embeddings', true)); } + /** + * Makes an embedding of a Doc. This Doc shares the data portion of the origiginal Doc. + * If the copied Doc has no prototype, then instead of copying the Doc, this just creates + * a new Doc that is a delegate of the original Doc. + * @param doc Doc to embed + * @param id id to use for embedded Doc + * @returns a new Doc that is an embedding of the original Doc + */ export function MakeEmbedding(doc: Doc, id?: string) { - const embedding = (!GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto) || doc.type === DocumentType.CONFIG ? 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)); - } + const embedding = (!Doc.IsDataProto(doc) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); embedding.createdFrom = doc; - embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; - !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); embedding.author = ClientUtils.CurrentUserEmail(); + embedding.proto_embeddingId = doc.$proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; + embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); return embedding; } export function BestEmbedding(doc: Doc) { - const dataDoc = doc[DocData]; - const availableEmbeddings = Doc.GetEmbeddings(dataDoc); - const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail()); + const availableEmbeddings = Doc.GetEmbeddings(doc); + const bestEmbedding = [...(doc[DocData] !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail()); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(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, - cloneTemplates: boolean - ): Promise<Doc> { + const FindDocsInRTF = new RegExp(/(audioId|textId|anchorId|docId)"\s*:\s*"(.*?)"/g); + + export 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, cloneTemplates: boolean): Doc { if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) { return doc; } @@ -722,81 +741,59 @@ export namespace Doc { const copy = new Doc(undefined, true); cloneMap.set(doc[Id], copy); const filter = [...exclusions, ...StrListCast(doc.cloneFieldFilter)]; - await Promise.all( - Object.keys(doc).map(async key => { - if (filter.includes(key)) return; - const assignKey = (val: Opt<FieldType>) => { - copy[key] = val; - }; - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = async (objField: 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, pruneDocs, cloneLinks, cloneTemplates))); - assignKey(new List<Doc>(clones)); - } else { - assignKey(ObjectField.MakeCopy(objField)); - if (objField instanceof RichTextField) { - if (DocsInTextFieldIds.some(id => objField.Data.includes(`"${id}":`))) { - const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g'); - const rawdocids = objField.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 rdocs = results && Array.from(Object.keys(results)).map(rkey => DocCast(results.get(rkey))); - rdocs?.map(d => d && Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); - rtfs.push({ copy, key, field: objField }); - } - } - } - }; - const docAtKey = doc[key]; + Object.keys(doc) + .filter(key => !filter.includes(key)) + .map(key => { + const assignKey = (val: Opt<FieldType>) => (copy[key] = val); + if (key === 'author') { - assignKey(ClientUtils.CurrentUserEmail()); - } else if (docAtKey instanceof Doc) { - if (pruneDocs.includes(docAtKey)) { - // prune doc and do nothing - } else if ( - !Doc.IsSystem(docAtKey) && - (key.includes('layout[') || - key.startsWith('layout') || // - ['embedContainer', 'annotationOn', 'proto'].includes(key) || - (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail())) - ) { - assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); - } else { - assignKey(docAtKey); + return assignKey(ClientUtils.CurrentUserEmail()); + } + const cfield = ComputedField.DisableCompute(() => doc[key]); + if (cfield instanceof ComputedField) { + return assignKey(cfield[Copy]()); + } + const field = doc[key]; + if (field instanceof Doc) { + const doCopy = () => Doc.IsSystem(field) || + !( key.startsWith('layout') || + ['embedContainer', 'annotationOn', 'proto'].includes(key) || // + (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail()) ); // prettier-ignore + return !pruneDocs.includes(field) && + assignKey(doCopy() + ? field // + : Doc.makeClone(field, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); // prettier-ignore + } + if (field instanceof RichTextField) { + rtfs.push({ copy, key, field }); + let docId: string | undefined; + while ((docId = (FindDocsInRTF.exec(field.Data) ?? [undefined, undefined, undefined])[2])) { + const docCopy = DocServer.GetCachedRefField(docId); + docCopy && Doc.makeClone(docCopy, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates); } - } else if (field instanceof RefField) { - assignKey(field); - } else if (cfield instanceof ComputedField) { - assignKey(cfield[Copy]()); - } else if (field instanceof ObjectField) { - await copyObjectField(field); - } else if (field instanceof Promise) { - // eslint-disable-next-line no-debugger - debugger; // This shouldn't happen... - } else { - assignKey(field); + return assignKey(ObjectField.MakeCopy(field)); } - }) - ); - Array.from(doc[DirectLinks]).forEach(async link => { + if (field instanceof ObjectField) { + if (DocListCast(field).length) { + return assignKey(new List<Doc>(DocListCast(field).map(d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)))); + } + return assignKey(ObjectField.MakeCopy(field)); // otherwise just copy the field + } + if (!(field instanceof Promise)) return assignKey(field); + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happen... + }); + Array.from(doc[DirectLinks]).forEach(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]))) + ((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, cloneTemplates)); + linkMap.set(link[Id], Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } }); copy.cloneOf = doc; - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc.title)); + const cfield = ComputedField.DisableCompute(() => FieldValue(doc.title)); if (Doc.Get(copy, 'title', true) && !(cfield instanceof ComputedField)) copy.title = '>:' + doc.title; cloneMap.set(doc[Id], copy); @@ -823,25 +820,27 @@ export namespace Doc { return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneTemplates, cloneMap)); } - export async function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) { + /** + * Copies a Doc and all of the Docs that it references. This is a deep copy of the Doc. + * However, the additional flags allow you to control whether to copy links and templates. + * @param doc Doc to clone + * @param cloneLinks whether to clone links to this Doc + * @param cloneTemplates whether to clone the templates used by this Doc + * @param cloneMap a map from the Doc ids of the original Doc to the new Docs + * @returns a clone of the original Doc + */ + export function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const clone = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); + const clone = Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], DocCast(doc.embedContainer) ? [DocCast(doc.embedContainer)!] : [], cloneLinks, cloneTemplates); const repaired = new Set<Doc>(); const linkedDocs = Array.from(linkMap.values()); linkedDocs.forEach(link => Doc.AddLink?.(link, true)); rtfMap.forEach(({ copy, key, field }) => { - const replacer = (match: string, attr: string, id: string /* , offset: any, string: any */) => { - const mapped = cloneMap.get(id); - return attr + '"' + (mapped ? mapped[Id] : id) + '"'; - }; - const replacer2 = (match: string, href: string, id: string /* , offset: any, string: any */) => { - const mapped = cloneMap.get(id); - return href + (mapped ? mapped[Id] : id); - }; + const replacer = (match: string, attr: string, id: string) => attr + '":"' + (cloneMap.get(id)?.[Id] ?? id) + '"'; + const replacer2 = (match: string, href: string, id: string) => href + (cloneMap.get(id)?.[Id] ?? id); 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); + copy[key] = new RichTextField(field.Data.replace(FindDocsInRTF, replacer).replace(re, replacer2), field.Text); }); const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; clonedDocs.forEach(cloneDoc => Doc.repairClone(cloneDoc, cloneMap, cloneTemplates, repaired)); @@ -849,34 +848,40 @@ export namespace Doc { } const _pendingMap = new Set<string>(); - // - // 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. - export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: 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 + * @param templateLayoutDoc a rendering template Doc + * @param targetDoc the Doc that the render template will be applied to + * @param layoutFieldKey the accumulated layoutFieldKey for the container of this expanded template + * @returns a Doc to use to render the targetDoc in the style of the template layout + */ + export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, layoutFieldKey?: string) { // 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 || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) { return templateLayoutDoc; } - const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutFieldKey(templateLayoutDoc)); // the field that the template renders + const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutDataKey(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 expandedLayoutFieldKey = templateField + '_layout[' + templateLayoutDoc[Id] + ']'; + const expandedLayoutFieldKey = expandedFieldName(templateLayoutDoc, layoutFieldKey); let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; - if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { + if (templateLayoutDoc.rootDocument instanceof Promise) { expandedTemplateLayout = undefined; _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); } else if (expandedTemplateLayout === undefined && !_pendingMap.has(targetDoc[Id] + expandedLayoutFieldKey)) { - if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) { + if (DocCast(templateLayoutDoc.rootDocument)?.[DocData] === targetDoc[DocData]) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { // eslint-disable-next-line no-param-reassign - templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype + templateLayoutDoc.rootDocument && (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.add(targetDoc[Id] + expandedLayoutFieldKey); setTimeout( @@ -885,7 +890,7 @@ export namespace Doc { const dataDoc = Doc.GetProto(targetDoc); newLayoutDoc.rootDocument = targetDoc; newLayoutDoc.embedContainer = targetDoc; - newLayoutDoc.resolvedDataDoc = dataDoc; + newLayoutDoc.cloneOnCopy = true; newLayoutDoc.acl_Guest = SharingPermissions.Edit; if (dataDoc[templateField] === undefined && (templateLayoutDoc[templateField] as List<Doc>)?.length) { dataDoc[templateField] = ObjectField.MakeCopy(templateLayoutDoc[templateField] as List<Doc>); @@ -902,87 +907,78 @@ export namespace Doc { return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplateLayout is pending. } - // if the childDoc is a template for a field, then this will return the expanded layout with its data doc. - // otherwise, it just returns the childDoc - export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) { + /** + * Returns a layout and data Doc pair to use to render the specified childDoc of the container. + * if the childDoc is a template for a field, then this will return the expanded layout with its data doc. + * otherwise, only a layout is returned since that will contain the data as its prototype. + * @param containerDoc the template container (that may be nested within a template, the root of a template, or not a template) + * @param containerDataDoc the template container's data doc (if the container is nested within the template) + * @param childDoc the doc to render + * @param layoutFieldKey the accumulated layoutFieldKey for the container of the doc being rendered + * @returns a layout Doc to render and an optional data Doc if the layout is a template + */ + export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc, layoutFieldKey?: string) { if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) { console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; + const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; const templateRoot = DocCast(containerDoc?.rootDocument); - return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc }; + return { layout: Doc.expandTemplateLayout(childDoc, templateRoot, layoutFieldKey), data }; } - export function FindReferences(infield: Doc | List<Doc>, references: Set<Doc>, system: boolean | undefined) { - if (infield instanceof Promise) return; - if (!(infield instanceof Doc)) { - infield?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system)); - return; + /** + * Recursively travels through all the metadata reachable from the Doc to find all referenced Docs + * @param doc Doc to search + * @param references all Docs reachable from the Doc + * @param system whether to include system Docs + * @returns all Docs reachable from the Doc + */ + export function FindReferences(doc: Doc | List<Doc> | undefined, references: Set<Doc>, system: boolean | undefined) { + if (!doc || doc instanceof Promise) { + return references; + } + if (!(doc instanceof Doc)) { + doc?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system)); + return references; } - const doc = infield as Doc; if (references.has(doc)) { - references.add(doc); - return; + return references; + } + if (system !== undefined && ((system && !Doc.IsSystem(doc)) || (!system && Doc.IsSystem(doc)))) { + return references; } - const excludeLists = [Doc.MyRecentlyClosed, Doc.MyHeaderBar, Doc.MyDashboards].includes(doc); - if (system !== undefined && ((system && !Doc.IsSystem(doc)) || (!system && Doc.IsSystem(doc)))) return; + references.add(doc); - Object.keys(doc).forEach(key => { - if (key === 'proto') { - if (doc.proto instanceof Doc) { - Doc.FindReferences(doc.proto, references, system); + Object.keys(doc) + .filter(key => key !== 'author' && !(ComputedField.DisableCompute(() => FieldValue(doc[key])) instanceof ComputedField)) + .map(key => doc[key]) + .forEach(field => { + if (field instanceof List) { + return ![Doc.MyRecentlyClosed, Doc.MyHeaderBar, Doc.MyDashboards].includes(doc) && Doc.FindReferences(field, references, system); } - } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); - if (field instanceof RefField) { - if (field instanceof Doc) { - if (key === 'myLinkDatabase') { - field instanceof Doc && references.add(field); - // skip docs that have been closed and are scheduled for garbage collection - } else { - Doc.FindReferences(field, references, system); - } - } - } else if (cfield instanceof ComputedField) { - /* empty */ - } else if (field instanceof ObjectField) { - if (field instanceof Doc) { - Doc.FindReferences(field, references, system); - } else if (field instanceof List) { - !excludeLists && Doc.FindReferences(field, references, system); - } else if (field instanceof ProxyField) { - if (key === 'myLinkDatabase') { - field instanceof Doc && references.add(field); - // skip docs that have been closed and are scheduled for garbage collection - } else { - Doc.FindReferences(field.value, references, system); - } - } else if (field instanceof PrefetchProxy) { - Doc.FindReferences(field.value, references, system); - } else if (field instanceof RichTextField) { - const re = /"docId"\s*:\s*"(.*?)"/g; - let match: string[] | null; - while ((match = re.exec(field.Data)) !== null) { - const urlString = match[1]; - if (urlString) { - const rdoc = DocServer.GetCachedRefField(urlString); - if (rdoc) { - references.add(rdoc); - Doc.FindReferences(rdoc, references, system); - } - } - } + if (field instanceof Doc) { + return Doc.FindReferences(field, references, system); + } + if (field instanceof RichTextField) { + let docId: string | undefined; + while ((docId = (FindDocsInRTF.exec(field.Data) ?? [undefined, undefined, undefined])[2])) { + Doc.FindReferences(DocServer.GetCachedRefField(docId), references, system); } - } else if (field instanceof Promise) { - // eslint-disable-next-line no-debugger - debugger; // This shouldn't happend... } - } - }); + }); + return references; } + /** + * Copies a Doc by copying its embedding (and optionally its prototype). Values within the Doc are copied except for Docs which are + * sipmly referenced (except if they're marked to be copied - eg., template layout Docs) + * @param doc Doc to copy + * @param copyProto whether to copy the Docs proto + * @param copyProtoId the id to use for the proto if copied + * @param retitle whether to retitle the copy by adding a copy number to the title + * @returns the copied Doc + */ export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = runInAction(() => new Doc(copyProtoId, true)); updateCachedAcls(copy); @@ -995,24 +991,18 @@ export namespace Doc { copy[key] = Doc.MakeCopy(doc.proto, false); } } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.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) { - const docAtKey = doc[key]; - copy[key] = - docAtKey instanceof Doc && (key.includes('layout[') || docAtKey.cloneOnCopy) - ? new ProxyField(Doc.MakeCopy(docAtKey)) // copy the expanded render template - : ObjectField.MakeCopy(field); - } else if (field instanceof Promise) { + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : doc[key]; + if (field instanceof Promise) { // eslint-disable-next-line no-debugger debugger; // This shouldn't happend... - } else { - copy[key] = field; } + copy[key] = (() => { + const cfield = ComputedField.DisableCompute(() => FieldValue(doc[key])); + if (cfield instanceof ComputedField) return cfield[Copy](); + if (field instanceof Doc) return field.cloneOnCopy ? Doc.MakeCopy(field) : field; // copy the expanded render template + if (field instanceof ObjectField) return ObjectField.MakeCopy(field); + return field; + })(); } }); if (copyProto) { @@ -1028,6 +1018,14 @@ export namespace Doc { return copy; } + /** + * Makes a delegate of a prototype Doc. Delegates inherit all of the properties of the + * prototype Doc, but can add new properties or mask existing prototype properties. + * @param doc prototype Doc to make a delgate of + * @param id id to use for delegate + * @param title title to use for delegate + * @returns a new Doc that is a delegate of the original Doc + */ export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> { @@ -1063,14 +1061,14 @@ export namespace Doc { return ndoc; } - let _applyCount: number = 0; export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { const proto = new Doc(); + const applyCount = NumCast(templateDoc.dragFactory_count); proto.author = ClientUtils.CurrentUserEmail(); const target = Doc.MakeDelegate(proto); const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); - const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); + const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + applyCount + ')'); target.layout_fieldKey = targetKey; //this and line above applied && (Doc.GetProto(applied).type = templateDoc.type); return applied; @@ -1080,7 +1078,7 @@ export namespace Doc { export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) { if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { - if (target.resolvedDataDoc) { + if (target.rootDocument) { target[targetKey] = new PrefetchProxy(templateDoc); } else { titleTarget && (Doc.GetProto(target).title = titleTarget); @@ -1097,13 +1095,13 @@ export namespace Doc { // export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>, keepFieldKey = false): boolean { // find the metadata field key that this template field doc will display (indicated by its title) - const metadataFieldKey = keepFieldKey ? Doc.LayoutFieldKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField); + const metadataFieldKey = keepFieldKey ? Doc.LayoutDataKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutDataKey(templateField); // update the original template to mark it as a template templateField.isTemplateForField = metadataFieldKey; !keepFieldKey && (templateField.title = metadataFieldKey); - const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)]; + const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutDataKey(templateField)]; // move any data that the template field had been rendering over to the template doc so that things will still be rendered // when the template field is adjusted to point to the new metadatafield key. // note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well. @@ -1112,11 +1110,15 @@ export namespace Doc { Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc)); Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue); } + if (templateField.type === DocumentType.IMG) { + // bcz: should be a better way .. but, if the image is a template, then we can't expect to know the aspect ratio. When the image is replaced by data and rendered, we want to recomputed the native dimensions. + templateField[DocData].layout_resetNativeDim = true; + } // get the layout string that the template uses to specify its layout - const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField))); + const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout])); // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document. - Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); + templateField[DocLayout].layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); return true; } @@ -1149,58 +1151,96 @@ export namespace Doc { @observable _searchQuery: string = ''; } - // the document containing the view layout information - will be the Document itself unless the Document has - // 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[DocLayout] || doc; - } - export function SetLayout(doc: Doc, layout: Doc | string) { - doc[StrCast(doc.layout_fieldKey, 'layout')] = layout; + /** + * The layout Doc containing the view layout information - this will be : + * a) the Doc being rendered itself unless + * b) a template Doc stored in the field sepcified by Doc's layout_fieldKey + * c) or the specifeid 'template' Doc; + * If a template is specified, it will be expanded to create an instance specific to the rendered doc; + * @param doc the doc to render + * @param template a Doc to use as a template for the layout + * @returns + */ + export function LayoutDoc(doc: Doc, template?: Doc): Doc { + const expandedTemplate = template && Cast(doc[expandedFieldName(template)], Doc, null); + return expandedTemplate || doc[DocLayout]; } + /** + * The JSX or Doc value defining how to render the Doc. + * @param doc Doc to render + * @returns a JSX string or Doc that describes how to render the Doc + */ export function LayoutField(doc: Doc) { return doc[StrCast(doc.layout_fieldKey, 'layout')]; } - export function LayoutFieldKey(doc: Doc, templateLayoutString?: string): string { - const match = StrCast(templateLayoutString || Doc.Layout(doc).layout).match(/fieldKey={'([^']+)'}/); - return match?.[1] || ''; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey + /** + * The field key of the Doc where the primary Data can be found to render the Doc. + * eg., for an image, this is likely to be the 'data' field which contains an image url, + * and for a text doc, this is likely to be the 'text' field ontaingin the text of the doc. + * @param doc Doc to render + * @param templateLayoutString optional JSX string that specifies the doc's data field key + * @returns field key where data is stored on Doc + */ + export function LayoutDataKey(doc: Doc, templateLayoutString?: string): string { + const match = StrCast(templateLayoutString || doc[DocLayout].layout).match(/fieldKey={'([^']+)'}/); + return match?.[1] || ''; } export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) { return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1); } export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { - return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0)); + // if this is a field template, then don't use the doc's nativeWidth/height + return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0)); } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { if (!doc) return 0; const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width - const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0); - return NumCast(doc._nativeHeight, nheight || dheight); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0); + // if this is a field template, then don't use the doc's nativeWidth/height + return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight); + } + + export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { + return !doc ? 0 : NumCast(doc._outpaintingWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingWidth'], useWidth ? NumCast(doc._width) : 0)); + } + + export function OutpaintingHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { + if (!doc) return 0; + const oheight = (Doc.OutpaintingWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingHeight'], useHeight ? NumCast(doc._height) : 0); + return NumCast(doc._outpaintingHeight, oheight || dheight); + } + + export function SetOutpaintingWidth(doc: Doc, width: number | undefined, fieldKey?: string) { + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingWidth'] = width; + } + + export function SetOutpaintingHeight(doc: Doc, height: number | undefined, fieldKey?: string) { + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingHeight'] = height; } + export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { - doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_nativeWidth'] = width; + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_nativeWidth'] = width; } export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { - doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_nativeHeight'] = height; + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_nativeHeight'] = height; } const manager = new UserDocData(); - export function SearchQuery(): string { + export function SearchQuery() { return manager._searchQuery; } export function SetSearchQuery(query: string) { - runInAction(() => { - manager._searchQuery = query; - }); + manager._searchQuery = query; } - export function UserDoc(): Doc { + export function UserDoc() { return manager._user_doc; } - export function SharingDoc(): Doc { + export function SharingDoc() { return Doc.MySharedDocs; } - export function LinkDBDoc(): Doc { - return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); + export function LinkDBDoc() { + return DocCast(Doc.UserDoc().myLinkDatabase); } export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); @@ -1225,7 +1265,7 @@ export namespace Doc { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(doc[DocData]) === AclPrivate) return doc; const result = brushManager.SearchMatchDoc.get(doc); const num = Math.abs(result?.searchMatch || 0) + 1; - runInAction(() => result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num })); + result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num }); return doc; } export function ClearSearchMatches() { @@ -1298,12 +1338,12 @@ export namespace Doc { highlightedDocs.add(doc); doc[Highlight] = true; doc[Animation] = presentationEffect; - if (dataAndDisplayDocs && !doc.resolvedDataDoc) { + if (dataAndDisplayDocs && !doc.rootDocument) { // if doc is a layout template then we don't want to highlight the proto since that will be the entire template, not just the specific layout field highlightedDocs.add(doc[DocData]); doc[DocData][Highlight] = true; // want to highlight the targets of presentation docs explicitly since following a pres target does not highlight PDf <Annotations> which are not DocumentViews - if (doc.presentation_targetDoc) DocCast(doc.presentation_targetDoc)[Highlight] = true; + if (DocCast(doc.presentation_targetDoc)) DocCast(doc.presentation_targetDoc)![Highlight] = true; } }); } @@ -1315,23 +1355,13 @@ export namespace Doc { highlightedDocs.delete(doc[DocData]); doc[Highlight] = doc[DocData][Highlight] = false; doc[Animation] = undefined; - if (doc.presentation_targetDoc) DocCast(doc.presentation_targetDoc)[Highlight] = false; + if (DocCast(doc.presentation_targetDoc)) DocCast(doc.presentation_targetDoc)![Highlight] = false; }); }); } export function getDocTemplate(doc?: Doc) { - return !doc - ? undefined - : doc.isTemplateDoc - ? doc - : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc - ? doc.dragFactory - : Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc - ? Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc - ? Doc.Layout(doc).proto - : Doc.Layout(doc) - : undefined; + return !doc ? undefined : doc.isTemplateDoc ? doc : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory : doc[DocLayout].isTemplateDoc ? (doc[DocLayout].rootDocument ? doc[DocLayout].proto : doc[DocLayout]) : undefined; } export function toggleLockedPosition(doc: Doc) { @@ -1352,7 +1382,7 @@ export namespace Doc { export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: readonly number[], modifiers?: 'remove') { if (!container) return; - const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []); + const childFiltersByRanges = StrListCast(container._childFiltersByRanges); for (let i = 0; i < childFiltersByRanges.length; i += 3) { if (childFiltersByRanges[i] === key) { @@ -1360,7 +1390,7 @@ export namespace Doc { break; } } - if (range !== undefined) { + if (range) { childFiltersByRanges.push(key); childFiltersByRanges.push(range[0].toString()); childFiltersByRanges.push(range[1].toString()); @@ -1415,7 +1445,7 @@ export namespace Doc { }); } export function readDocRangeFilter(doc: Doc, key: string) { - const childFiltersByRanges = Cast(doc._childFiltersByRanges, listSpec('string'), []); + const childFiltersByRanges = StrListCast(doc._childFiltersByRanges); for (let i = 0; i < childFiltersByRanges.length; i += 3) { if (childFiltersByRanges[i] === key) { return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])]; @@ -1450,8 +1480,9 @@ export namespace Doc { DocServer.GetRefFields(docids).then(async fieldlist => { const list = Array.from(fieldlist.values()) .map(d => DocCast(d)) - .filter(d => d); - const docs = clone ? (await Promise.all(Doc.MakeClones(list, false, false))).map(res => res.clone) : list; + .filter(d => d) + .map(d => d!); + const docs = clone ? Doc.MakeClones(list, false, 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; @@ -1471,16 +1502,22 @@ export namespace Doc { * @returns */ export function getDescription(doc: Doc) { - const curDescription = StrCast(doc[DocData][Doc.LayoutFieldKey(doc) + '_description']); + const curDescription = StrCast(doc['$' + Doc.LayoutDataKey(doc) + '_description']); const docText = (async (tdoc:Doc) => { switch (tdoc.type) { case DocumentType.PDF: return curDescription || StrCast(tdoc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text - case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '') + case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(tdoc[Doc.LayoutDataKey(tdoc)], '_o') ?? '') .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.')); - case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text; + case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutDataKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutDataKey(tdoc)]); default: return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title); }}); // prettier-ignore - return docText(doc).then(text => (doc[DocData][Doc.LayoutFieldKey(doc) + '_description'] = text)); + return docText(doc).then( + action(text => { + // set the time when the date changes. This also allows a live textbox view to react to the update, otherwise, it wouldn't take effect until the next time the view is rerendered. + doc['$' + Doc.LayoutDataKey(doc) + '_description_modificationDate'] = new DateField(); + return (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text); + }) + ); } // prettier-ignore @@ -1728,12 +1765,12 @@ export function IdToDoc(id: string) { return DocCast(DocServer.GetCachedRefField(id)); } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function idToDoc(id: string): Doc { +ScriptingGlobals.add(function idToDoc(id: string): Doc | undefined { return IdToDoc(id); }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renameEmbedding(doc: Doc) { - return StrCast(doc[DocData].title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; + return StrCast(doc.title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getProto(doc: Doc) { @@ -1782,7 +1819,7 @@ ScriptingGlobals.add(function docCastAsync(doc: FieldResult): FieldResult<Doc> { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function activePresentationItem() { const curPres = Doc.ActivePresentation; - return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; + return curPres && DocListCast(curPres[Doc.LayoutDataKey(curPres)])[NumCast(curPres._itemIndex)]; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: string, modifiers: 'match' | 'check' | 'x' | 'remove') { diff --git a/src/fields/List.ts b/src/fields/List.ts index 22bbcb9ab..ba03c0d38 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -255,7 +255,7 @@ export class ListImpl<T extends FieldType> extends ObjectField { if (fields) { this[SelfProxy].push(...fields); } - // eslint-disable-next-line no-constructor-return + return list; // need to return the proxy here, otherwise we don't get any of our list handler functions } @@ -317,8 +317,8 @@ export class ListImpl<T extends FieldType> extends ObjectField { // declare List as a type so you can use it in type declarations, e.g., { l: List, ...} export type List<T extends FieldType> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; -// decalre List as a value so you can invoke 'new' on it, e.g., new List<Doc>() (since List<T> IS ListImpl<T>, we can safely cast the 'new' return value to return List<T>) -// eslint-disable-next-line no-redeclare +// declare List as a value so you can invoke 'new' on it, e.g., new List<Doc>() (since List<T> IS ListImpl<T>, we can safely cast the 'new' return value to return List<T>) +// eslint-disable-next-line no-use-before-define export const List: { new <T extends FieldType>(fields?: T[]): List<T> } = ListImpl as unknown as { new <T extends FieldType>(fields?: T[]): List<T> }; ScriptingGlobals.add('List', List); diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 48c336e60..21b9fe89b 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -16,9 +16,26 @@ function deserializeProxy(field: serializedProxyType) { } @Deserializable('proxy', (obj: unknown) => deserializeProxy(obj as serializedProxyType)) export class ProxyField<T extends Doc> extends ObjectField { + @serializable(primitive()) + readonly fieldId: string = ''; + + private failed = false; + + // 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; p: FieldWaiting<T> | undefined } = { field: undefined, p: undefined }; + private get cache(): { field: T | undefined; p: FieldWaiting<T> | undefined } { + return this._cache; + } + private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) { + runInAction(() => (this._cache = { ...val })); + } + constructor(); constructor(value: T); constructor(fieldId: string); + constructor(value: T | string); constructor(value?: T | string) { super(); if (typeof value === 'string') { @@ -30,13 +47,12 @@ export class ProxyField<T extends Doc> extends ObjectField { } } - [ToValue](/* doc: any */) { + [ToValue]() { return ProxyField.toValue(this); } [Copy]() { - if (this.cache.field) return new ProxyField<T>(this.cache.field); - return new ProxyField<T>(this.fieldId); + return new ProxyField<T>(this.cache.field ?? this.fieldId); } [ToJavascriptString]() { @@ -46,27 +62,9 @@ export class ProxyField<T extends Doc> extends ObjectField { return Field.toScriptString(this[ToValue]()?.value as T); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? } [ToString]() { - return Field.toString(this[ToValue]()?.value); + return Field.toString(this[ToValue]()?.value as T); } - @serializable(primitive()) - readonly fieldId: string = ''; - - // 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; p: FieldWaiting<T> | undefined } = { field: undefined, p: undefined }; - private get cache(): { field: T | undefined; p: FieldWaiting<T> | undefined } { - return this._cache; - } - private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) { - runInAction(() => { - this._cache = { ...val }; - }); - } - - private failed = false; - @computed get value(): T | undefined | FieldWaiting<T> { if (this.cache.field) return this.cache.field; if (this.failed) return undefined; @@ -94,32 +92,19 @@ export class ProxyField<T extends Doc> extends ObjectField { return field; } } - -// eslint-disable-next-line no-redeclare, @typescript-eslint/no-namespace export namespace ProxyField { let useProxy = true; - export function DisableProxyFields() { + export function DisableDereference<T>(fn: () => T) { useProxy = false; - } - - export function EnableProxyFields() { - useProxy = true; - } - - export function WithoutProxy<T>(fn: () => T) { - DisableProxyFields(); try { return fn(); } finally { - EnableProxyFields(); + useProxy = true; } } export function toValue(value: { value: unknown }) { - if (useProxy) { - return { value: value.value }; - } - return undefined; + return useProxy ? { value: value.value } : undefined; } } @@ -129,5 +114,6 @@ function prefetchValue(proxy: PrefetchProxy<Doc>) { } @scriptingGlobal -@Deserializable('prefetch_proxy', (obj:unknown) => prefetchValue(obj as PrefetchProxy<Doc>)) +// eslint-disable-next-line no-use-before-define +@Deserializable('prefetch_proxy', (obj: unknown) => prefetchValue(obj as PrefetchProxy<Doc>)) export class PrefetchProxy<T extends Doc> extends ProxyField<T> {} diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index bcef1fefc..68a3737bf 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -42,7 +42,8 @@ export class RichTextField extends ObjectField { return this.Text; } - // AARAV ADD= + // AARAV ADD + static ToProsemirrorDoc = (content: Record<string, unknown>[], selection: Record<string, unknown>) => ({ doc: { type: 'doc', @@ -86,7 +87,33 @@ export class RichTextField extends ObjectField { { type: 'text', anchor: 2 + plaintext.length - (selectBack ?? 0), head: 2 + plaintext.length } ); + // AARAV ADD + + // takes in text segments instead of single text field + private static ToProsemirrorSegmented = (textSegments: { text: string; styles?: { bold?: boolean; italic?: boolean; fontSize?: number; color?: string } }[], imgDocId?: string, selectBack?: number) => + RichTextField.ToProsemirrorDoc( + textSegments.map(seg => ({ + type: 'paragraph', // Each segment becomes its own paragraph + content: [...RichTextField.ToProsemirrorTextContent(seg.text, seg.styles), ...(imgDocId ? RichTextField.ToProsemirrorDashDocContent(imgDocId) : [])], + })), + (textLen => ({ + type: 'text', + anchor: textLen - (selectBack ?? 0), + head: textLen, + }))(2 * textSegments.length + textSegments.map(seg => seg.text).join('').length - 1) + // selection/doc end = text length + 2 for each paragraph. subtract 1 to set selection inside of end of last paragraph + ); + + // AARAV ADD || + public static textToRtf(text: string, imgDocId?: string, styles?: { bold?: boolean; italic?: boolean; fontSize?: number; color?: string }, selectBack?: number) { return new RichTextField(JSON.stringify(RichTextField.ToProsemirror(text, imgDocId, styles, selectBack)), text); } + + // AARAV ADD + public static textToRtfFormat(textSegments: { text: string; styles?: { bold?: boolean; italic?: boolean; fontSize?: number; color?: string } }[], imgDocId?: string, selectBack?: number) { + return new RichTextField(JSON.stringify(RichTextField.ToProsemirrorSegmented(textSegments, imgDocId, selectBack)), textSegments.map(seg => seg.text).join('')); + } + + // AARAV ADD } diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 42dd0d432..e16073ff4 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -20,6 +20,7 @@ import { Doc, Opt } from './Doc'; import { Id } from './FieldSymbols'; import { RichTextField } from './RichTextField'; import { Cast, StrCast } from './Types'; +import { Upload } from '../server/SharedMediaTypes'; export namespace RichTextUtils { const delimiter = '\n'; @@ -127,7 +128,7 @@ export namespace RichTextUtils { return { baseUrl: embeddedObject.imageProperties!.contentUri! }; }); - const uploads = await Networking.PostToServer('/googlePhotosMediaGet', { mediaItems }); + const uploads = (await Networking.PostToServer('/googlePhotosMediaGet', { mediaItems })) as Upload.FileInformation[]; if (uploads.length !== mediaItems.length) { throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: 'Error with internally uploading inlineObjects!' }); diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 89e5cda8d..cdd278d67 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -1,6 +1,3 @@ -/* eslint-disable guard-for-in */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable no-redeclare */ /* eslint-disable no-use-before-define */ import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types'; import { Doc, FieldType } from './Doc'; @@ -16,7 +13,6 @@ type AllToInterface<T extends Interface[]> = { export const emptySchema = createSchema({}); export const Document = makeInterface(emptySchema); -// eslint-disable-next-line no-redeclare export type Document = makeInterface<[typeof emptySchema]>; export interface InterfaceFunc<T extends Interface[]> { @@ -36,9 +32,10 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu const proto = new Proxy( {}, { - get(target: any, prop, receiver) { + get(target: unknown, prop, receiver) { const field = receiver.doc?.[prop]; if (prop in schema) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any 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 @@ -59,7 +56,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu } return field; }, - set(target: any, prop, value, receiver) { + set(target: unknown, prop, value, receiver) { receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls return true; }, @@ -77,10 +74,10 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) const type = schema[key]; Object.defineProperty(proto, key, { get() { - return Cast(this.__doc[key], type as any); + return Cast(this.__doc[key], type as never); }, set(setValue) { - const value = Cast(setValue, type as any); + const value = Cast(setValue, type as never); if (value !== undefined) { this.__doc[key] = value; return; @@ -89,7 +86,7 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) }, }); } - return function (doc: any) { + return function (doc: unknown) { if (!(doc instanceof Doc)) { throw new Error("Currently wrapping a schema in another schema isn't supported"); } @@ -101,18 +98,18 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) // eslint-disable-next-line @typescript-eslint/no-unused-vars export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } { - return undefined as any; + return undefined as never; // (schema as any).proto = Doc; // return schema as any; } export function listSpec<U extends ToConstructor<FieldType>>(type: U): ListSpec<ToType<U>> { - return { List: type as any }; // TODO Types + return { List: type as never }; // TODO Types } export function defaultSpec<T extends ToConstructor<FieldType>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> { return { - type: type as any, + type: type as never, defaultVal, }; } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index b294ee8c6..37b65684b 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -149,7 +149,6 @@ export class ScriptField extends ObjectField { ...params, }, transformer, - typecheck: false, editable: true, addReturn: addReturn, capturedVariables, @@ -189,14 +188,12 @@ export class ScriptField extends ObjectField { export class ComputedField extends ScriptField { static undefined = '__undefined'; static useComputed = true; - static DisableComputedFields() { this.useComputed = false; } // prettier-ignore - static EnableComputedFields() { this.useComputed = true; } // prettier-ignore - static WithoutComputed<T>(fn: () => T) { - this.DisableComputedFields(); + static DisableCompute<T>(fn: () => T) { + this.useComputed = false; try { return fn(); } finally { - this.EnableComputedFields(); + this.useComputed = true; } } @@ -210,6 +207,7 @@ export class ComputedField extends ScriptField { this._lastComputedResult = this._cachedResult ?? computedFn(() => + ((val) => val instanceof Array ? new List<number>(val) : val)( this.script.compiled && this.script.run( { @@ -220,7 +218,7 @@ export class ComputedField extends ScriptField { _readOnly_: true, }, console.log - ).result as FieldResult + ).result as FieldResult) )(); // prettier-ignore return this._lastComputedResult; }; @@ -238,7 +236,8 @@ export class ComputedField extends ScriptField { public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) { if (!doc[`${fieldKey}_indexed`]) { const flist = new List<number>(numberRange(curTimecode + 1).map(emptyFunction) as unknown as number[]); - flist[curTimecode] = Cast(doc[fieldKey], 'number', null); + if (Cast(doc[fieldKey], 'number', null) === undefined) delete flist[curTimecode]; + else flist[curTimecode] = Cast(doc[fieldKey], 'number', null)!; doc[`${fieldKey}_indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey}, ${defaultVal})`, {}, true, {}); diff --git a/src/fields/Types.ts b/src/fields/Types.ts index 474882959..ba2e9bb6f 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -1,5 +1,6 @@ import { DateField } from './DateField'; import { Doc, FieldType, FieldResult, Opt } from './Doc'; +import { InkField } from './InkField'; import { List } from './List'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -8,7 +9,7 @@ import { ScriptField } from './ScriptField'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; // eslint-disable-next-line no-use-before-define -export type ToConstructor<T extends FieldType> = 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 ToConstructor<T extends FieldType> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: never[]) => T; export type DefaultFieldConstructor<T extends FieldType> = { type: ToConstructor<T>; @@ -17,7 +18,7 @@ export type DefaultFieldConstructor<T extends FieldType> = { // type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> }; export type ListSpec<T extends FieldType> = { List: ToConstructor<T> }; -export type InterfaceValue = ToConstructor<FieldType> | ListSpec<FieldType> | DefaultFieldConstructor<FieldType> | ((doc?: Doc) => any); +export type InterfaceValue = ToConstructor<FieldType> | ListSpec<FieldType> | DefaultFieldConstructor<FieldType> | ((doc?: Doc) => never); export type ToType<T extends InterfaceValue> = T extends 'string' ? string @@ -31,9 +32,9 @@ export type ToType<T extends InterfaceValue> = T extends 'string' // eslint-disable-next-line @typescript-eslint/no-unused-vars T extends DefaultFieldConstructor<infer _U> ? never - : T extends { new (...args: any[]): List<FieldType> } + : T extends { new (...args: never[]): List<FieldType> } ? never - : T extends { new (...args: any[]): infer R } + : T extends { new (...args: never[]): infer R } ? R : T extends (doc?: Doc) => infer R ? R @@ -41,44 +42,40 @@ export type ToType<T extends InterfaceValue> = T extends 'string' export interface Interface { [key: string]: InterfaceValue; - // [key: string]: ToConstructor<Field> | ListSpec<Field[]>; } 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]>>; }; -// 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 Head<T extends unknown[]> = T extends [unknown, ...unknown[]] ? T[0] : Interface; +export type Tail<T extends unknown[]> = ((...t: T) => unknown) extends (_: unknown, ...tail: infer TT) => unknown ? TT : []; +export type HasTail<T extends unknown[]> = T extends [] | [unknown] ? false : true; // TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial -export type WithoutRefField<T extends FieldType> = T extends RefField ? never : T; +export type WithoutRefField<T extends FieldType> = T extends RefField ? unknown : T; export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>; type WithoutList<T extends FieldType> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T; -export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>; -// eslint-disable-next-line no-redeclare -export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; -// eslint-disable-next-line no-redeclare +export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>> | undefined; +export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<ToType<T>> | null): WithoutList<ToType<T>> | undefined; 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 ToType<T>) as ToType<T>) : defaultVal === null ? undefined : defaultVal; } if (field !== undefined && !(field instanceof Promise)) { if (typeof ctor === 'string') { - // eslint-disable-next-line valid-typeof if (typeof field === ctor) { return field as ToType<T>; } } else if (typeof ctor === 'object') { if (field instanceof List) { - return field as any; + return field as ToType<T>; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } else if (field instanceof (ctor as any)) { return field as ToType<T>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } else if (field instanceof ProxyField && field.value instanceof (ctor as any)) { return field.value as ToType<T>; } @@ -86,63 +83,36 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal return defaultVal === null ? undefined : defaultVal; } -export function toList(doc: Doc | Doc[]) { - return doc instanceof Doc ? [doc] : doc; -} +export function toList(doc: Doc | Doc[]) { return doc instanceof Doc ? [doc] : doc; } // prettier-ignore export function DocCast(field: FieldResult, defaultVal?: Doc) { - 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); -} - -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); -} -export function DateCast(field: FieldResult) { - return Cast(field, DateField, null); -} -export function RTFCast(field: FieldResult) { - return Cast(field, RichTextField, null); -} - -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); -} -export function VideoCast(field: FieldResult, defaultVal: VideoField | null = null) { - return Cast(field, VideoField, defaultVal); -} -export function AudioCast(field: FieldResult, defaultVal: AudioField | null = null) { - return Cast(field, AudioField, defaultVal); -} -export function PDFCast(field: FieldResult, defaultVal: PdfField | null = null) { - return Cast(field, PdfField, defaultVal); -} -export function ImageCast(field: FieldResult, defaultVal: ImageField | null = null) { - return Cast(field, ImageField, defaultVal); + return ((doc: Doc | undefined) => (doc && !(doc instanceof Promise) ? doc : defaultVal))(Cast(field, Doc, null)); +} +export function NumCast (field: FieldResult, defaultVal: number | null = 0) { return Cast(field, 'number', defaultVal)!; } // prettier-ignore +export function StrCast (field: FieldResult, defaultVal: string | null = '') { return Cast(field, 'string', defaultVal)!; } // prettier-ignore +export function BoolCast (field: FieldResult, defaultVal: boolean | null = false) { return Cast(field, 'boolean', defaultVal)!; } // prettier-ignore +export function DateCast (field: FieldResult, defaultVal: DateField | null = null) { return Cast(field, DateField, defaultVal); } // prettier-ignore +export function RTFCast (field: FieldResult, defaultVal: RichTextField | null = null){ return Cast(field, RichTextField, defaultVal); } // prettier-ignore +export function ScriptCast(field: FieldResult, defaultVal: ScriptField | null = null) { return Cast(field, ScriptField, defaultVal); } // prettier-ignore +export function CsvCast (field: FieldResult, defaultVal: CsvField | null = null) { return Cast(field, CsvField, defaultVal); } // prettier-ignore +export function WebCast (field: FieldResult, defaultVal: WebField | null = null) { return Cast(field, WebField, defaultVal); } // prettier-ignore +export function VideoCast (field: FieldResult, defaultVal: VideoField | null = null) { return Cast(field, VideoField, defaultVal); } // prettier-ignore +export function AudioCast (field: FieldResult, defaultVal: AudioField | null = null) { return Cast(field, AudioField, defaultVal); } // prettier-ignore +export function PDFCast (field: FieldResult, defaultVal: PdfField | null = null) { return Cast(field, PdfField, defaultVal); } // prettier-ignore +export function ImageCast (field: FieldResult, defaultVal: ImageField | null = null) { return Cast(field, ImageField, defaultVal); } // prettier-ignore +export function InkCast (field: FieldResult, defaultVal: InkField | null = null) { return Cast(field, InkField, defaultVal); } // prettier-ignore + +export function ImageCastToNameType(field: FieldResult, defaultVal: ImageField | null = null) { + const href = ImageCast(field, defaultVal)?.url.href; + return href ? [href.replace(/.[^.]*$/, ''), href.split('.').lastElement()] : ['', '']; } export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) { - const href = ImageCast(field, defaultVal)?.url.href; - return href ? `${href.split('.')[0]}${suffix}.${href.split('.')[1]}` : null; + const [name, type] = ImageCastToNameType(field, defaultVal); + return name ? `${name}${suffix}.${type}` : null; } export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>; -// eslint-disable-next-line no-redeclare export function FieldValue<T extends FieldType>(field: FieldResult<T>): Opt<T>; -// eslint-disable-next-line no-redeclare export function FieldValue<T extends FieldType>(field: FieldResult<T>, defaultValue?: T): Opt<T> { return field instanceof Promise || field === undefined ? defaultValue : field; } diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 3a83e7ca0..8dedb0be7 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -17,9 +17,7 @@ export abstract class URLField extends ObjectField { readonly url: URL; constructor(urlVal: string); - // eslint-disable-next-line @typescript-eslint/no-shadow constructor(urlVal: URL); - // eslint-disable-next-line @typescript-eslint/no-shadow constructor(urlVal: URL | string) { super(); this.url = @@ -50,6 +48,7 @@ export abstract class URLField extends ObjectField { } [Copy](): this { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return new (this.constructor as any)(this.url); } } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index b27816f55..df832d088 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -35,8 +35,6 @@ export const documentSchema = createSchema({ _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 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 diff --git a/src/fields/util.ts b/src/fields/util.ts index 33764aca5..799fa3758 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -241,19 +241,19 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDoc[DirectLinks].forEach(link => distributeAcls(key, acl, link, visited, !!allowUpgrade)); - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { + DocListCast(dataDoc[Doc.LayoutDataKey(dataDoc)]).forEach(d => { distributeAcls(key, acl, d, visited, !!allowUpgrade); d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade); }); - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => { + DocListCast(dataDoc[Doc.LayoutDataKey(dataDoc) + '_annotations']).forEach(d => { distributeAcls(key, acl, d, visited, !!allowUpgrade); d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade); }); - Object.keys(target) // share expanded layout templates (eg, for presElementBox'es ) - .filter(lkey => lkey.includes('layout[') && DocCast(target[lkey])) - .forEach(lkey => distributeAcls(key, acl, DocCast(target[lkey]), visited, !!allowUpgrade)); + Object.keys(target) // share expanded layout templates (eg, for PresSlideBox'es ) + .filter(lkey => lkey.includes('layout_[') && DocCast(target[lkey])) + .forEach(lkey => distributeAcls(key, acl, DocCast(target[lkey])!, visited, !!allowUpgrade)); if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; @@ -303,23 +303,23 @@ export function SetPropSetterCb(prop: string, propSetter: ((target: Doc, value: // // target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List. // -export function setter(target: ListImpl<FieldType> | Doc, inProp: string | symbol | number, value: unknown, receiver: Doc | ListImpl<FieldType>): boolean { - if (!inProp) { +export function setter(target: ListImpl<FieldType> | Doc, prop: string | symbol | number, value: unknown, receiver: Doc | ListImpl<FieldType>): boolean { + if (!prop) { console.log('WARNING: trying to set an empty property. This should be fixed. '); return false; } - let prop = inProp; - const effectiveAcl = inProp === 'constructor' || typeof inProp === 'symbol' ? AclAdmin : GetPropAcl(target, prop); + const effectiveAcl = prop === 'constructor' || typeof prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl_') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value as SharingPermissions))) return true; - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { - if (!prop.startsWith('__')) prop = prop.substring(1); - if (target.__LAYOUT__ instanceof Doc) { - target.__LAYOUT__[prop] = value as FieldResult; - return true; - } + if (target instanceof Doc && typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('$')) { + target.__DATA__[prop.substring(1)] = value as FieldResult; + return true; + } + if (target instanceof Doc && typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_') && !prop.startsWith('__')) { + target.__LAYOUT__[prop.substring(1)] = value as FieldResult; + return true; } if (target.__fieldTuples[prop] instanceof ComputedField) { if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { @@ -351,6 +351,7 @@ export function getter(target: Doc | ListImpl<FieldType>, prop: string | symbol, case DocAcl : return target[DocAcl]; case $mobx: return target.__fieldTuples[prop]; case DocLayout: return target.__LAYOUT__; + case DocData: return target.__DATA__; case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; // eslint-disable-next-line no-fallthrough default : @@ -362,6 +363,8 @@ export function getter(target: Doc | ListImpl<FieldType>, prop: string | symbol, const layoutProp = prop.startsWith('_') ? prop.substring(1) : undefined; if (layoutProp && target.__LAYOUT__) return (target.__LAYOUT__ as Doc)[layoutProp]; + const dataProp = prop.startsWith('$') ? prop.substring(1) : undefined; + if (dataProp && target.__DATA__) return (target.__DATA__ as Doc)[dataProp]; return getFieldImpl(target, layoutProp ?? prop, proxy); } |