diff options
Diffstat (limited to 'src/fields')
| -rw-r--r-- | src/fields/Doc.ts | 486 | ||||
| -rw-r--r-- | src/fields/List.ts | 6 | ||||
| -rw-r--r-- | src/fields/Proxy.ts | 64 | ||||
| -rw-r--r-- | src/fields/Schema.ts | 21 | ||||
| -rw-r--r-- | src/fields/ScriptField.ts | 12 | ||||
| -rw-r--r-- | src/fields/Types.ts | 91 | ||||
| -rw-r--r-- | src/fields/util.ts | 10 |
7 files changed, 329 insertions, 361 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 2db8d3b85..abce7ed26 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'; @@ -47,7 +47,7 @@ export namespace Field { * @returns string representation of the field */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean, schemaCell?: boolean): string { - 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 @@ -133,13 +133,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 { @@ -176,13 +176,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 { @@ -266,22 +280,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) { + 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.$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 + 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) { @@ -407,20 +427,17 @@ export class Doc extends RefField { } @computed get __DATA__(): Doc { const self = this[SelfProxy]; - return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(Cast(self[DocLayout].rootDocument, Doc, null) || self); + return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(DocCast(self[DocLayout].rootDocument, self)!); } @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['layout_' + templateLayoutDoc.title + '(' + renderFieldKey + ')'], Doc, null) || templateLayoutDoc; + return DocCast(self[expandedFieldName(templateLayoutDoc)], templateLayoutDoc)!; } return self; } @@ -597,7 +614,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 { @@ -630,7 +647,7 @@ 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 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); @@ -647,7 +664,7 @@ 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 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) { @@ -686,6 +703,14 @@ 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 = (!Doc.IsDataProto(doc) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); embedding.createdFrom = doc; @@ -705,17 +730,9 @@ export namespace 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; } @@ -723,80 +740,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.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 +819,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,23 +847,29 @@ 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 = 'layout_' + templateLayoutDoc.title + '(' + templateField + ')'; + const expandedLayoutFieldKey = expandedFieldName(templateLayoutDoc, layoutFieldKey); let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; if (templateLayoutDoc.rootDocument instanceof Promise) { @@ -876,7 +880,7 @@ export namespace Doc { 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.rootDocument && (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,6 +889,7 @@ export namespace Doc { const dataDoc = Doc.GetProto(targetDoc); newLayoutDoc.rootDocument = targetDoc; newLayoutDoc.embedContainer = targetDoc; + 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>); @@ -901,87 +906,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 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 }; + 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); @@ -994,24 +990,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) { @@ -1027,6 +1017,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> { @@ -1096,13 +1094,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. @@ -1148,55 +1146,74 @@ 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, template?: Doc): Doc { - const expandedTemplate = template && Cast(doc['layout_' + template.title + '(' + StrCast(template.isTemplateForField, 'data') + ')'], Doc, null); + /** + * 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 { + /** + * 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] || ''; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_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)); + return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], 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); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(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.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); @@ -1221,7 +1238,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() { @@ -1299,7 +1316,7 @@ export namespace Doc { 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; } }); } @@ -1311,7 +1328,7 @@ 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; }); }); } @@ -1338,7 +1355,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) { @@ -1346,7 +1363,7 @@ export namespace Doc { break; } } - if (range !== undefined) { + if (range) { childFiltersByRanges.push(key); childFiltersByRanges.push(range[0].toString()); childFiltersByRanges.push(range[1].toString()); @@ -1401,7 +1418,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])]; @@ -1436,8 +1453,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; @@ -1457,16 +1475,16 @@ export namespace Doc { * @returns */ export function getDescription(doc: Doc) { - const curDescription = StrCast(doc['$' + 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(tdoc[Doc.LayoutFieldKey(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 ?? StrCast(tdoc[Doc.LayoutFieldKey(tdoc)]); + 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['$' + Doc.LayoutFieldKey(doc) + '_description'] = text)); + return docText(doc).then(text => (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text)); } // prettier-ignore @@ -1714,7 +1732,7 @@ 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 @@ -1768,7 +1786,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/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 6e2d7eb78..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; } } @@ -239,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 dc7516f38..e6755f828 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -8,7 +8,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 +17,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 +31,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,29 +41,26 @@ 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>>; -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): 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') { @@ -72,10 +69,12 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal } } 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>; } @@ -83,57 +82,27 @@ 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); -} + 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 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); -} export function ImageCastToNameType(field: FieldResult, defaultVal: ImageField | null = null) { const href = ImageCast(field, defaultVal)?.url.href; - return href ? [href.replace(/.[^.]*$/, ''), href.split('.').lastElement()] : ["", ""]; + return href ? [href.replace(/.[^.]*$/, ''), href.split('.').lastElement()] : ['', '']; } export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) { const [name, type] = ImageCastToNameType(field, defaultVal); diff --git a/src/fields/util.ts b/src/fields/util.ts index 205c500a5..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; |
