diff options
81 files changed, 880 insertions, 887 deletions
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 5a8230847..7845c80aa 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -55,13 +55,13 @@ export namespace DocUtils { const matchLink = (val: string, anchor: Doc) => { const linkedToExp = (val ?? '').split('='); if (linkedToExp.length === 1) return Field.toScriptString(anchor) === val; - return Field.toScriptString(DocCast(anchor[linkedToExp[0]])) === linkedToExp[1]; + return DocCast(anchor[linkedToExp[0]]) && Field.toScriptString(DocCast(anchor[linkedToExp[0]])!) === linkedToExp[1]; }; // prettier-ignore return (value === Doc.FilterNone && !allLinks.length) || (value === Doc.FilterAny && !!allLinks.length) || - (allLinks.some(link => matchLink(value as string, DocCast(link.link_anchor_1)) || - matchLink(value as string, DocCast(link.link_anchor_2)) )); + (allLinks.some(link => (DocCast(link.link_anchor_1) && matchLink(value as string, DocCast(link.link_anchor_1)!)) || + (DocCast(link.link_anchor_2) && matchLink(value as string, DocCast(link.link_anchor_2)!)) )); } if (typeof value === 'string') { value = value.replace(`,${ClientUtils.noRecursionHack}`, ''); @@ -266,7 +266,7 @@ export namespace DocUtils { Object.keys(funcs) .filter(key => !key.endsWith('-setter')) .forEach(key => { - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const cfield = ComputedField.DisableCompute(() => FieldValue(doc[key])); const func = funcs[key]; if (ScriptCast(cfield)?.script.originalScript !== func) { const setFunc = Cast(funcs[key + '-setter'], 'string', null); @@ -375,12 +375,13 @@ export namespace DocUtils { const documentList: ContextMenuProps[] = foo .filter(btnDoc => !btnDoc.hidden) - .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) + .map(btnDoc => DocCast(btnDoc?.dragFactory)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title) + .map(doc => doc!) .map(dragDoc => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable(() => { - const newDoc = (Doc.isTemplateDoc(dragDoc) ? DocUtils.delegateDragFactory : DocUtils.copyDragFactory)(dragDoc); + const newDoc = (dragDoc.isTemplateDoc ? DocUtils.delegateDragFactory : DocUtils.copyDragFactory)(dragDoc); if (newDoc) { newDoc._author = ClientUtils.CurrentUserEmail(); newDoc.x = x; @@ -437,6 +438,7 @@ export namespace DocUtils { .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title) + .map(doc => doc!) .map(dragDoc => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable(() => { @@ -494,11 +496,11 @@ export namespace DocUtils { .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc) .filter(d => d.isTemplateDoc); // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized - // first try to find a template that matches the specific document type (<typeName>_<templateName>). otherwise, fallback to a general match on <templateName> + // first try to find a template that matches the specific document type (<typeName><TemplateName>). otherwise, fallback to a general match on <templateName> !docLayoutTemplate && allTemplates.forEach(tempDoc => { const templateType = StrCast(doc[templateName + '_fieldKey'] || doc.type); - StrCast(tempDoc.title) === templateName + '_' + templateType && (docLayoutTemplate = tempDoc); + StrCast(tempDoc.title) === templateName + (templateType[0].toUpperCase() + templateType.slice(1)) && (docLayoutTemplate = tempDoc); }); !docLayoutTemplate && allTemplates.forEach(tempDoc => { @@ -522,18 +524,13 @@ export namespace DocUtils { Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined); } } else { - let fieldTemplate: Opt<Doc>; - if (doc.data instanceof RichTextField || typeof doc.data === 'string') { - fieldTemplate = Docs.Create.TextDocument('', options); - } else if (doc.data instanceof PdfField) { - fieldTemplate = Docs.Create.PdfDocument('http://www.msn.com', options); - } else if (doc.data instanceof VideoField) { - fieldTemplate = Docs.Create.VideoDocument('http://www.cs.brown.edu', options); - } else if (doc.data instanceof AudioField) { - fieldTemplate = Docs.Create.AudioDocument('http://www.cs.brown.edu', options); - } else if (doc.data instanceof ImageField) { - fieldTemplate = Docs.Create.ImageDocument('http://www.cs.brown.edu', options); - } + const fieldTemplate = (() => { + if (doc.data instanceof RichTextField || typeof doc.data === 'string') return Docs.Create.TextDocument('', options); + if (doc.data instanceof PdfField) return Docs.Create.PdfDocument('http://www.msn.com', options); + if (doc.data instanceof VideoField) return Docs.Create.VideoDocument('http://www.cs.brown.edu', options); + if (doc.data instanceof AudioField) return Docs.Create.AudioDocument('http://www.cs.brown.edu', options); + if (doc.data instanceof ImageField) return Docs.Create.ImageDocument('http://www.cs.brown.edu', options); + })(); const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + '(' + doc.title + ')', isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); @@ -904,8 +901,8 @@ export namespace DocUtils { return ndoc; } - export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') { - const { clone, map, linkMap } = await Doc.MakeClone(doc); + export function Zip(doc: Doc, zipFilename = 'dashExport.zip') { + const { clone, map, linkMap } = Doc.MakeClone(doc); const proms = new Set<string>(); function replacer(key: string, value: { url: string; [key: string]: unknown }) { if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined; diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index dd0985182..5c6559836 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -36,7 +36,7 @@ export enum DocumentType { // special purpose wrappers that either take no data or are compositions of lower level types LINK = 'link', PRES = 'presentation', - PRESELEMENT = 'preselement', + PRESSLIDE = 'presslide', COMPARISON = 'comparison', PUSHPIN = 'pushpin', MAPROUTE = 'maproute', diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d93a432d4..bf9cc5bd4 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -217,7 +217,7 @@ export class DocumentOptions { author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable author_date?: DATEt = new DateInfo('date the document was created', true); annotationOn?: DOCt = new DocInfo('document annotated by this document', false); - rootDocument?: DOCt = new DocInfo('document that supplies the information needed for a rendering template (eg, pres slide for PresElement)'); + rootDocument?: DOCt = new DocInfo('document that stores the data for compound template documents.'); color?: STRt = new StrInfo('foreground color data doc', false); hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false); backgroundColor?: STRt = new StrInfo('background color for data doc', false); @@ -1139,8 +1139,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); } - export function PresElementBoxDocument() { - return Prototypes.get(DocumentType.PRESELEMENT); + export function PresSlideDocument() { + return Prototypes.get(DocumentType.PRESSLIDE); } export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) { @@ -1148,7 +1148,7 @@ export namespace Docs { } export function AnnoPaletteDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyStickers]), { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([...(Doc.MyStickers ? [Doc.MyStickers] : [])]), { ...(options || {}) }); } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 35dc5f1c7..1887c1716 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -148,12 +148,12 @@ export class CurrentUserUtils { { title: "Idea", backgroundColor: "pink", icon: "lightbulb" , _layout_showTitle: "title"}, { title: "Topic", backgroundColor: "lightblue", icon: "book-open" , _layout_showTitle: "title"}]; - + const metanote = DocCast(doc.emptyMetaNote); const reqdNoteList = [...reqdTempOpts.map(opts => { const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true}; const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(fdoc => fdoc.title === opts.title): undefined; return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts)); - }), DocCast(doc.emptyMetaNote), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; + }), ...(metanote ? [metanote]:[]), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); @@ -211,7 +211,7 @@ export class CurrentUserUtils { const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); const makeIconTemplate = (name: DocumentType | string | undefined, templateField: string, opts:DocumentOptions) => { - const title = "icon" + (name ? "_" + name : ""); + const title = "icon" + (name ? name[0].toUpperCase()+name.slice(1) : ""); const curIcon = DocCast(templateIconsDoc[title]); const creator = (() => { switch (opts.iconTemplate) { case DocumentType.IMG : return imageBox; @@ -424,7 +424,10 @@ pie title Minerals in my tap water const standardOps = (key:string) => ({ title : "Untitled "+ key, _layout_fitWidth: false, isSystem: true, "dragFactory_count": 0, cloneFieldFilter: new List<string>(["isSystem"]) }); emptyThings.forEach( thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs)); - DocCast(Doc.UserDoc().emptyMetaNote).title = "MetaNote"; // hack: metanotes are used a template, so 'untitled metaNote' is an awkward name + const metanote = DocCast(Doc.UserDoc().emptyMetaNote); + if (metanote) { + metanote.title = "MetaNote"; // hack: metanotes are used a template, so 'untitled metaNote' is an awkward name + } return [ { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)}, @@ -490,7 +493,7 @@ pie title Minerals in my tap water { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", }, { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field - { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue: badgeValue}}, + { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs??Doc.UserDoc(), icon: "users", funcs: {badgeValue: badgeValue}}, { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}}, { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), icon: "folder-open", hidden: false }, { title: "Faces", toolTip: "Unique Faces", target: this.setupFaceCollection(doc, "myFaceCollection"), icon: "face-smile", hidden: false }, @@ -551,7 +554,6 @@ pie title Minerals in my tap water const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined); const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined; const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools); - // doc.myUserBtns = new PrefetchProxy(userBtns); const reqdToolOps:DocumentOptions = { title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", layout_explainer: "This is a palette of documents that can be created.", _layout_dontCenter: "y", @@ -975,7 +977,10 @@ pie title Minerals in my tap water }; DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts); - if (!Doc.GetProto(DocCast(doc.mySharedDocs)).data_dashboards) Doc.GetProto(DocCast(doc.mySharedDocs)).data_dashboards = new List<Doc>(); + const sharedDocs = DocCast(doc.mySharedDocs); + if (sharedDocs) { + if (!Doc.GetProto(sharedDocs).data_dashboards) Doc.GetProto(sharedDocs).data_dashboards = new List<Doc>(); + } } /// Import option on the left side button panel @@ -1000,9 +1005,9 @@ pie title Minerals in my tap water static updateUserDocument(docIn: Doc, sharingDocumentId: string, linkDatabaseId: string) { const doc = docIn; DocUtils.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {}); - reaction(() => DateCast(DocCast(doc.globalGroupDatabase).data_modificationDate), + reaction(() => DateCast(DocCast(doc.globalGroupDatabase)?.data_modificationDate), async () => { - const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); + const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase)?.data); const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(ClientUtils.CurrentUserEmail())) || []; SetCachedGroups(["Guest", ...(mygroups?.map(g => StrCast(g.title))??[])]); }, { fireImmediately: true }); @@ -1058,11 +1063,13 @@ pie title Minerals in my tap water SelectionManager.DeselectAll(); // this forces SelectionManager implementation to copy over to DocumentView's API. This also triggers the LinkManager to be created - Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards); - Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs); - Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed); + if (Doc.MyFilesystem) { + Doc.MyDashboards && Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards); + Doc.MyDashboards && Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards); + Doc.MyRecentlyClosed && Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed); + } - Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)).data = new WebField("https://www.wikipedia.org"); + DocCast(Doc.UserDoc().emptyWebpage) && (Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)!).data = new WebField("https://www.wikipedia.org")); DocServer.CacheNeedsUpdate() && setTimeout(UPDATE_SERVER_CACHE, 2500); setInterval(UPDATE_SERVER_CACHE, 120000); @@ -1142,7 +1149,7 @@ pie title Minerals in my tap water const file = input.files?.[0]; if (file?.type === 'application/zip' || file?.type === 'application/x-zip-compressed') { const doc = await Doc.importDocument(file); - const list = Cast(Doc.MyImports.data, listSpec(Doc), null); + const list = Cast(Doc.MyImports?.data, listSpec(Doc), null); doc instanceof Doc && list?.splice(0, 0, doc); } else if (input.files && input.files.length !== 0) { const disposer = OverlayView.ShowSpinner(); @@ -1150,7 +1157,7 @@ pie title Minerals in my tap water if (results.length !== input.files?.length) { alert("Error uploading files - possibly due to unsupported file types"); } - const list = Cast(Doc.MyImports.data, listSpec(Doc), null); + const list = Cast(Doc.MyImports?.data, listSpec(Doc), null); list?.splice(0, 0, ...results); disposer(); } else { diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 2eef3da0e..dcef4a4fe 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -364,7 +364,7 @@ export namespace DictationManager { case 'nested collection':return Docs.Create.FreeformDocument([], {}); } // prettier-ignore })(); - created && Doc.AddDocToList(target.dataDoc, Doc.LayoutFieldKey(target.Document), created); + created && Doc.AddDocToList(target.dataDoc, Doc.LayoutDataKey(target.Document), created); } } }, diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 5ce005811..ad57c2a62 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -183,7 +183,7 @@ export class DocumentManager { static _howl: Howl; static playAudioAnno(doc: Doc) { - const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '_audioAnnotations'], listSpec(AudioField), null)?.lastElement(); + const anno = Cast(doc[Doc.LayoutDataKey(doc) + '_audioAnnotations'], listSpec(AudioField), null)?.lastElement(); if (anno) { this._howl?.stop(); if (anno instanceof AudioField) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index e2e4c0fe4..a66e6998b 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -234,29 +234,27 @@ export namespace DragManager { dropDoc instanceof Doc && CreateLinkToActiveAudio(() => dropDoc); return dropDoc; }; - const finishDrag = async (e: DragCompleteEvent) => { + const finishDrag = (e: DragCompleteEvent) => { const { docDragData } = e; setTimeout(() => dragData.dragEnding?.()); onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; - docDragData.droppedDocuments = ( - await Promise.all( - dragData.draggedDocuments.map(async d => - !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) - ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result as Doc) - : docDragData.dropAction === dropActionType.embed - ? Doc.BestEmbedding(d) - : docDragData.dropAction === dropActionType.add - ? d - : docDragData.dropAction === dropActionType.proto - ? d[DocData] - : docDragData.dropAction === dropActionType.copy - ? (await Doc.MakeClone(d)).clone - : d - ) + docDragData.droppedDocuments = dragData.draggedDocuments + .map(d => + !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) + ? addAudioTag(ScriptCast(d.onDragStart)!.script.run({ this: d }).result as Doc) + : docDragData.dropAction === dropActionType.embed + ? Doc.BestEmbedding(d) + : docDragData.dropAction === dropActionType.add + ? d + : docDragData.dropAction === dropActionType.proto + ? d[DocData] + : docDragData.dropAction === dropActionType.copy + ? Doc.MakeClone(d).clone + : d ) - ).filter(d => d); + .filter(d => d); ![dropActionType.same, dropActionType.proto].includes(StrCast(docDragData.dropAction) as dropActionType) && docDragData.droppedDocuments // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any)) @@ -364,7 +362,7 @@ export namespace DragManager { }; } - async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { + function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { const dropArgs = { cancelable: true, // allows preventDefault() to be called to cancel the drop bubbles: true, @@ -380,7 +378,7 @@ export namespace DragManager { }; target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes) - await finishDrag?.(complete); + finishDrag?.(complete); UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called options?.dragComplete?.(complete); endDrag?.(); @@ -566,11 +564,11 @@ export namespace DragManager { const targClassName = e.target instanceof HTMLElement && typeof e.target.className === 'string' ? e.target.className : ''; if (['lm_tab', 'lm_title_wrap', 'lm_tabs', 'lm_header'].includes(targClassName) && docDragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { - startWindowDragTimer = setTimeout(async () => { + startWindowDragTimer = setTimeout(() => { startWindowDragTimer = undefined; docDragData.dropAction = docDragData.userDropAction || dropActionType.same; AbortDrag(); - await finishDrag?.(new DragCompleteEvent(true, docDragData)); + finishDrag?.(new DragCompleteEvent(true, docDragData)); DragManager.StartWindowDrag?.(e, docDragData.droppedDocuments, aborted => { if (!aborted && (docDragData?.dropAction === dropActionType.move || docDragData?.dropAction === dropActionType.same)) { docDragData.removeDocument?.(docDragData?.draggedDocuments[0]); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 7d3f63448..b6b111930 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -3,7 +3,7 @@ import { DocData, DocLayout } from '../../fields/DocSymbols'; import { ObjectField } from '../../fields/ObjectField'; import { RichTextField } from '../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; -import { StrCast } from '../../fields/Types'; +import { DocCast, StrCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { Docs, DocumentOptions } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; @@ -23,7 +23,7 @@ import { ScriptingGlobals } from './ScriptingGlobals'; */ function makeTemplate(doc: Doc, first: boolean = true): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; - if (layoutDoc.layout instanceof Doc) { + if (DocCast(layoutDoc.layout)) { return true; // its already a template } const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)?.[0]; @@ -68,7 +68,7 @@ export function MakeTemplate(doc: Doc) { * Makes a draggable button or image that will create a template doc Instance */ export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) { - const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + const layoutDoc = doc; if (layoutDoc.type !== DocumentType.FONTICON) { !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); } diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 9c32ca25a..eae3460b3 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -15,7 +15,7 @@ export namespace ImageUtils { export const AssignImgInfo = (document: Doc, data?: Upload.InspectionResults) => { if (data) { data.nativeWidth && (document._height = (NumCast(document._width) * data.nativeHeight) / data.nativeWidth); - const field = '$' + Doc.LayoutFieldKey(document); + const field = '$' + Doc.LayoutDataKey(document); document[`${field}_nativeWidth`] = data.nativeWidth; document[`${field}_nativeHeight`] = data.nativeHeight; document[`${field}_path`] = data.source; diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index 0e67dcfaa..6081c3fae 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -43,13 +43,13 @@ export class LinkFollower { }; public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) { - const getView = (doc: Doc) => DocumentView.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc)); - const isAnchor = (source: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, source) || Doc.AreProtosEqual(anchor.annotationOn as Doc, source); + const getView = (doc: Doc) => DocumentView.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc)!); + const isAnchor = (source?: Doc, anchor?: Doc) => Doc.AreProtosEqual(anchor, source) || Doc.AreProtosEqual(DocCast(anchor?.annotationOn), source); const linkDocs = link ? [link] : Doc.Links(sourceDoc); - const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1 - const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2 - const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2))); - const backLinkWithoutTargetView = backLinks.find(l => !getView(DocCast(l.link_anchor_1))); + const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, DocCast(l.link_anchor_1))); // link docs where 'sourceDoc' is link_anchor_1 + const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, DocCast(l.link_anchor_2))); // link docs where 'sourceDoc' is link_anchor_2 + const fwdLinkWithoutTargetView = fwdLinks.find(l => !DocCast(l.link_anchor_2) || !getView(DocCast(l.link_anchor_2)!)); + const backLinkWithoutTargetView = backLinks.find(l => !DocCast(l.link_anchor_1) || !getView(DocCast(l.link_anchor_1)!)); const linkWithoutTargetDoc = traverseBacklink === undefined ? (fwdLinkWithoutTargetView ?? backLinkWithoutTargetView) : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks; const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); @@ -60,17 +60,17 @@ export class LinkFollower { return false; } followLinks.forEach(async linkDoc => { - const target = ( + const target = DocCast( sourceDoc === linkDoc.link_anchor_1 ? linkDoc.link_anchor_2 : sourceDoc === linkDoc.link_anchor_2 ? linkDoc.link_anchor_1 - : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc) + : Doc.AreProtosEqual(sourceDoc, DocCast(linkDoc.link_anchor_1)) || Doc.AreProtosEqual(DocCast(DocCast(linkDoc.link_anchor_1)?.annotationOn), sourceDoc) ? linkDoc.link_anchor_2 : linkDoc.link_anchor_1 - ) as Doc; - const srcAnchor: Doc = Doc.getOppositeAnchor(linkDoc, target) ?? sourceDoc; + ); if (target) { + const srcAnchor = Doc.getOppositeAnchor(linkDoc, target) ?? sourceDoc; const doFollow = (canToggle?: boolean) => { const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle); const options: FocusViewOptions = { @@ -102,14 +102,16 @@ export class LinkFollower { if (srcAnchor.followLinkLocation === OpenWhere.inParent) { const sourceDocParent = DocCast(sourceDoc.embedContainer); if (target.embedContainer instanceof Doc && target.embedContainer !== sourceDocParent) { - Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutFieldKey(target.embedContainer), target); + Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutDataKey(target.embedContainer), target); movedTarget = true; } - if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) { - Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target); - movedTarget = true; + if (sourceDocParent) { + if (!DocListCast(sourceDocParent[Doc.LayoutDataKey(sourceDocParent)]).includes(target)) { + Doc.AddDocToList(sourceDocParent, Doc.LayoutDataKey(sourceDocParent), target); + movedTarget = true; + } + Doc.SetContainer(target, sourceDocParent); } - Doc.SetContainer(target, sourceDocParent); } const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) { diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 3f98ab3c4..344e2e4c0 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -136,7 +136,7 @@ export class LinkManager { true ); FieldLoader.ServerLoadStatus.message = 'links'; - this.addLinkDB(Doc.LinkDBDoc()); + Doc.LinkDBDoc() && this.addLinkDB(Doc.LinkDBDoc()!); } public createlink_relationshipLists = () => { @@ -148,16 +148,21 @@ export class LinkManager { public addLink(linkDoc: Doc, checkExists = false) { Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc); - if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { - Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc); - // eslint-disable-next-line no-use-before-define - setTimeout(UPDATE_SERVER_CACHE, 100); + if (Doc.LinkDBDoc()) { + if (!checkExists || !DocListCast(Doc.LinkDBDoc()!.data).includes(linkDoc)) { + Doc.AddDocToList(Doc.LinkDBDoc()!, 'data', linkDoc); + // eslint-disable-next-line no-use-before-define + setTimeout(UPDATE_SERVER_CACHE, 100); + } } } public deleteLink(linkDoc: Doc) { - const ret = Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc); - linkDoc.$link_anchor_1 = linkDoc.$link_anchor_2 = undefined; - return ret; + if (Doc.LinkDBDoc()) { + const ret = Doc.RemoveDocFromList(Doc.LinkDBDoc()!, 'data', linkDoc); + linkDoc.$link_anchor_1 = linkDoc.$link_anchor_2 = undefined; + return ret; + } + return false; } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(LinkManager.Instance.deleteLink); @@ -178,8 +183,8 @@ export class LinkManager { } const dirLinks = Array.from(anchor[DocData][DirectLinks]).filter(l => Doc.GetProto(anchor) === anchor[DocData] || ['1', '2'].includes(LinkManager.anchorIndex(l, anchor) as '0' | '1' | '2')); - const anchorRoot = DocCast(anchor.rootDocument, anchor); // template Doc fields store annotations on the topmost root of a template (not on themselves since the template layout items are only for layout) - const annos = DocListCast(anchorRoot[Doc.LayoutFieldKey(anchor) + '_annotations']); + const anchorRoot = DocCast(anchor.rootDocument, anchor)!; // template Doc fields store annotations on the topmost root of a template (not on themselves since the template layout items are only for layout) + const annos = DocListCast(anchorRoot[Doc.LayoutDataKey(anchor) + '_annotations']); return Array.from( annos.reduce((set, anno) => { if (!processed.includes(anno)) { @@ -242,11 +247,13 @@ export function UPDATE_SERVER_CACHE() { .filter(doc => doc instanceof Doc) .map(doc => doc as Doc); const references = new Set<Doc>(prototypes); + DocCast(Doc.UserDoc().myLinkDatabase) && references.add(DocCast(Doc.UserDoc().myLinkDatabase)!); // prevent crawling through link database here -- see below Doc.FindReferences(Doc.UserDoc(), references, undefined); - DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => { - if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) { - Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link); - Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link); + + DocListCast(DocCast(Doc.UserDoc().myLinkDatabase)?.data).forEach(link => { + if (DocCast(link.link_anchor_1) && !references.has(DocCast(link.link_anchor_1)!) && DocCast(link.link_anchor_2) && !references.has(DocCast(link.link_anchor_2)!)) { + DocCast(Doc.UserDoc().myLinkDatabase) && Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase)!, 'data', link); + Doc.MyRecentlyClosed && Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link); } }); LinkManager.Instance.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined)); @@ -257,10 +264,10 @@ export function UPDATE_SERVER_CACHE() { cacheDocumentIds = newCacheUpdate; // print out cached docs - //Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = '); - // const isFiltered = filtered.filter(doc => !Doc.IsSystem(doc)); - // const strings = isFiltered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)')); - //Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); + Doc.MyDockedBtns?.linearView_IsOpen && console.log('Set cached docs = '); + const isFiltered = filtered.filter(doc => !Doc.IsSystem(doc)); + const strings = isFiltered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)')); + Doc.MyDockedBtns?.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); rp.post(ClientUtils.prepend('/setCacheDocumentIds'), { body: { diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 733eae5f4..e4adcaa7e 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -9,7 +9,7 @@ export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; export function SearchCollection(collectionDoc: Opt<Doc>, queryIn: string, matchKeyNames: boolean, onlyKeys?: string[]) { - const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; + const blockedTypes = [DocumentType.PRESSLIDE, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = matchKeyNames ? [] : Object.entries(DocOptions) @@ -21,7 +21,7 @@ export namespace SearchUtil { const results = new ObservableMap<Doc, string[]>(); if (collectionDoc) { - const docs = DocListCast(collectionDoc[Doc.LayoutFieldKey(collectionDoc)]); + const docs = DocListCast(collectionDoc[Doc.LayoutDataKey(collectionDoc)]); // eslint-disable-next-line @typescript-eslint/ban-types const docIDs: String[] = []; SearchUtil.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { @@ -77,7 +77,7 @@ export namespace SearchUtil { // eslint-disable-next-line no-loop-func docs.filter(d => d && !visited.includes(d)).forEach(d => { visited.push(d); - const fieldKey = Doc.LayoutFieldKey(d); + const fieldKey = Doc.LayoutDataKey(d); const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView'); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; data && newarray.push(...DocListCast(data)); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 88f1f3260..9e79fd870 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -578,7 +578,7 @@ export class SettingsManager extends React.Component<object> { <div className="settings-username" style={{ color: SettingsManager.userBackgroundColor }}> {ClientUtils.CurrentUserEmail()} </div> - <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(ClientUtils.prepend('/logout'))} /> + <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} background={SettingsManager.userColor} onClick={() => window.location.assign(ClientUtils.prepend('/logout'))} /> </div> </div> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 45c80c6f7..1ecd82dbc 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -48,7 +48,7 @@ export function DocComponent<P extends DocComponentProps>() { * This Doc inherits from the dataDoc, and may or may not inherit (or be) the layoutDoc. */ get rootDoc() { - return DocCast(this.Document.rootDocument, this.Document); + return DocCast(this.Document.rootDocument, this.Document)!; } /** @@ -64,7 +64,7 @@ export function DocComponent<P extends DocComponentProps>() { * This may or may not inherit from the data doc. */ @computed get layoutDoc() { - return this._props.LayoutTemplateString ? this.Document : Doc.Layout(this.Document, this._props.LayoutTemplate?.()); + return this._props.LayoutTemplateString ? this.Document : Doc.LayoutDoc(this.Document, this._props.LayoutTemplate?.()); } /** @@ -115,7 +115,7 @@ export function ViewBoxBaseComponent<P extends FieldViewProps>() { * consider: Document, layoutDoc, dataDoc */ get rootDoc() { - return DocCast(this.Document.rootDocument, this.Document); + return DocCast(this.Document.rootDocument, this.Document)!; } /** * This is the document being rendered by the React component. In the @@ -189,7 +189,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { * other Doc options (Document, layoutDoc, dataDoc) */ @computed get rootDoc() { - return DocCast(this.Document.rootDocument, this.Document); + return DocCast(this.Document.rootDocument, this.Document)!; } /** * This is the document being rendered. It may be a template so it may or may no inherit from the data doc. diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 6f2f051cd..8a850467a 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -303,7 +303,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( DocumentView.Selected().forEach(dv => { dv.layoutDoc._layout_showTags = !showing; if (e.shiftKey) - DocListCast(dv.Document[Doc.LayoutFieldKey(dv.Document) + '_annotations']).forEach(doc => { + DocListCast(dv.Document[Doc.LayoutDataKey(dv.Document) + '_annotations']).forEach(doc => { if (doc.face) doc.hidden = showing; }); }); @@ -479,13 +479,13 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( const doc = rootView?.Document; if (doc) { const anchor = rootView.ComponentView?.getAnchor?.(true) ?? doc; - const trail = DocCast(anchor.presentationTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true); + const trail = DocCast(anchor.presentationTrail) ?? (DocCast(Doc.UserDoc().emptyTrail) ? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail)!, true) : undefined); if (trail !== anchor.presentationTrail) { - DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' }); + trail && DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' }); anchor.presentationTrail = trail; } Doc.ActivePresentation = trail; - this._props.views().lastElement()?._props.addDocTab(trail, OpenWhere.replaceRight); + trail && this._props.views().lastElement()?._props.addDocTab(trail, OpenWhere.replaceRight); } e.stopPropagation(); }; diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index c3b3f9d0c..848a77004 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -182,7 +182,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { return targetView?.ComponentView?.annotationKey || (targetView?.ComponentView?.fieldKey ?? 'data'); } @computed get targetDocChildren() { - return [...DocListCast(this.Document?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_sidebar'])]; + return [...DocListCast(this.Document?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.Document[Doc.LayoutDataKey(this.Document) + '_sidebar'])]; } @computed get rangeFilters() { @@ -236,7 +236,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++; facetVal !== undefined && valueSet.add(Field.toString(facetVal as FieldType)); (facetVal === true || facetVal === false) && valueSet.add(Field.toString(!facetVal)); - const fieldKey = Doc.LayoutFieldKey(t); + const fieldKey = Doc.LayoutDataKey(t); const annos = !Field.toString(Doc.LayoutField(t) as FieldType).includes('CollectionView'); DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 94c023330..948beec74 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -239,7 +239,7 @@ export class KeyManager { break; case 'i': { - const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); + const importBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyImports); if (importBtn) { MainView.Instance.selectLeftSidebarButton(importBtn); } @@ -247,7 +247,7 @@ export class KeyManager { break; case 's': { - const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); + const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTrails); if (trailsBtn) { MainView.Instance.selectLeftSidebarButton(trailsBtn); } @@ -257,7 +257,7 @@ export class KeyManager { if (DocumentView.Selected().length === 1 && DocumentView.Selected()[0].ComponentView?.search) { DocumentView.Selected()[0].ComponentView?.search?.('', false, false); } else { - const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher); + const searchBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MySearcher); if (searchBtn) { MainView.Instance.selectLeftSidebarButton(searchBtn); } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 6854476e2..425f5b6bb 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -341,7 +341,7 @@ export class InkStrokeProperties { */ snapControl = (inkView: DocumentView, controlIndex: number) => { const inkDoc = inkView.Document; - const ink = Cast(inkDoc[Doc.LayoutFieldKey(inkDoc)], InkField)?.inkData; + const ink = Cast(inkDoc[Doc.LayoutDataKey(inkDoc)], InkField)?.inkData; if (ink) { const screenDragPt = inkView.ComponentView?.ptToScreen?.(ink[controlIndex]); diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 1293aa9d0..2e6b477e9 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -118,9 +118,9 @@ export class InkTranscription extends React.Component { const times: number[] = []; validInks - .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField)) + .filter(i => Cast(i[Doc.LayoutDataKey(i)], InkField)) .forEach(i => { - const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null); + const d = Cast(i[Doc.LayoutDataKey(i)], InkField, null); const inkStroke = DocumentView.getDocumentView(i)?.ComponentView as InkingStroke; strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y }))); times.push(DateCast(i.author_date).getDate().getTime()); @@ -382,7 +382,7 @@ export class InkTranscription extends React.Component { render() { return ( - <div className="ink-transcription" style={{pointerEvents: 'none'}}> + <div className="ink-transcription" style={{ pointerEvents: 'none' }}> <div className="math-editor" ref={this.setMathRef}></div> <div className="text-editor" ref={this.setTextRef}></div> </div> diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index de2c7cd09..0eb21b943 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -136,7 +136,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { return this.SetLightboxDoc( doc, undefined, - [...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...this._future].sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)), + [...DocListCast(doc[Doc.LayoutDataKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...this._future].sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)), layoutTemplate ); }; @@ -187,7 +187,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { saved: this._savedState, }); if (this._docTarget) { - const fieldKey = Doc.LayoutFieldKey(this._docTarget); + const fieldKey = Doc.LayoutDataKey(this._docTarget); const contents = [...DocListCast(this._docTarget[fieldKey]), ...DocListCast(this._docTarget[fieldKey + '_annotations'])]; const links = Doc.Links(this._docTarget) .map(link => Doc.getOppositeAnchor(link, this._docTarget!)!) diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 22725a2b9..e4bbb1c0f 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -61,7 +61,7 @@ import { FootnoteView } from './nodes/formattedText/FootnoteView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { SummaryView } from './nodes/formattedText/SummaryView'; import { ImportElementBox } from './nodes/importBox/ImportElementBox'; -import { PresBox, PresElementBox } from './nodes/trails'; +import { PresBox, PresSlideBox } from './nodes/trails'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; import { StickerPalette } from './smartdraw/StickerPalette'; @@ -135,7 +135,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; AudioBox, RecordingBox, PresBox, - PresElementBox, + PresSlideBox, SearchBox, ImageLabelBox, FaceCollectionBox, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index be6e2fecb..ad6bb09c7 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -93,7 +93,7 @@ export class MainView extends ObservableReactComponent<object> { @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row) @observable private _dashUIHeight: number = 0; // height of entire main dashboard region including top menu buttons @observable private _panelContent: string = 'none'; - @observable private _sidebarContent: Doc = Doc.MyLeftSidebarPanel; + @observable private _sidebarContent?: Doc = Doc.MyLeftSidebarPanel; @observable private _leftMenuFlyoutWidth: number = 0; @computed get _hideUI() { return SnappingManager.HideUI || (this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking); @@ -173,7 +173,7 @@ export class MainView extends ObservableReactComponent<object> { views => views.length > 1 && document.activeElement instanceof HTMLElement && document.activeElement?.blur() ); reaction( - () => Doc.MyDockedBtns.linearView_IsOpen, + () => Doc.MyDockedBtns?.linearView_IsOpen, open => SnappingManager.SetPrintToConsole(!!open) ); const scriptTag = document.createElement('script'); @@ -199,7 +199,7 @@ export class MainView extends ObservableReactComponent<object> { ele.outerHTML = ''; }, 1000); } - this._sidebarContent.proto = undefined; + this._sidebarContent && (this._sidebarContent.proto = undefined); if (!MainView.Live) { DocServer.setLivePlaygroundFields([ 'dataTransition', @@ -636,8 +636,10 @@ export class MainView extends ObservableReactComponent<object> { openPresentation = (pres: Doc) => { if (pres.type === DocumentType.PRES) { CollectionDockingView.AddSplit(pres, OpenWhereMod.right, undefined, PresBox.PanelName); - Doc.MyTrails && (Doc.ActivePresentation = pres); - Doc.AddDocToList(Doc.MyTrails, 'data', pres); + if (Doc.MyTrails) { + Doc.ActivePresentation = pres; + Doc.AddDocToList(Doc.MyTrails, 'data', pres); + } this.closeFlyout(); } }; @@ -645,16 +647,16 @@ export class MainView extends ObservableReactComponent<object> { @action createNewFolder = async () => { const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }); - Doc.AddDocToList(Doc.MyFilesystem, 'data', folder); + Doc.MyFilesystem && Doc.AddDocToList(Doc.MyFilesystem, 'data', folder); }; waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined); headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1); - addHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); - removeHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true); + addHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => !!this.headerBarDoc && Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); + removeHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => !!this.headerBarDoc && Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true); @computed get headerBarDocView() { - return ( + return !this.headerBarDoc ? null : ( <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}> <DocumentView key="headerBarDoc" @@ -792,16 +794,16 @@ export class MainView extends ObservableReactComponent<object> { <div key="flyout" className="mainView-libraryFlyout-out"> {this.docButtons} </div> - ) : ( + ) : !this._sidebarContent ? null : ( <div key="libFlyout" className="mainView-libraryFlyout" style={{ minWidth: this._leftMenuFlyoutWidth, width: this._leftMenuFlyoutWidth }}> <div className="mainView-contentArea"> <DocumentView - Document={DocCast(this._sidebarContent.proto, this._sidebarContent)} + Document={DocCast(this._sidebarContent?.proto, this._sidebarContent)!} addDocument={undefined} addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={DocumentView.PinDoc} containerViewPath={returnEmptyDocViewList} - styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem || this._sidebarContent.proto === Doc.MyTrails ? DashboardStyleProvider : DefaultStyleProvider} + styleProvider={this._sidebarContent?.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem || this._sidebarContent.proto === Doc.MyTrails ? DashboardStyleProvider : DefaultStyleProvider} removeDocument={returnFalse} ScreenToLocalTransform={this.mainContainerXf} PanelWidth={this.leftMenuFlyoutWidth} @@ -821,7 +823,7 @@ export class MainView extends ObservableReactComponent<object> { } @computed get leftMenuPanel() { - return ( + return !Doc.MyLeftSidebarMenu ? null : ( <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: DocumentView.LightboxDoc() ? 'none' : undefined }}> <DocumentView Document={Doc.MyLeftSidebarMenu} @@ -861,7 +863,7 @@ export class MainView extends ObservableReactComponent<object> { break; default: this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; - this._sidebarContent.proto = DocCast(button.target); + this._sidebarContent && (this._sidebarContent.proto = DocCast(button.target)); SnappingManager.SetLastPressedBtn(button[Id]); } } @@ -875,8 +877,10 @@ export class MainView extends ObservableReactComponent<object> { */ addHotKey = (hotKey: string) => { const filterIcons = DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter); - const menuDoc = CurrentUserUtils.setupContextMenuBtn(CurrentUserUtils.filterBtnDesc(ToTagName(hotKey), 'question'), filterIcons); - Doc.AddToFilterHotKeys(menuDoc); + if (filterIcons) { + const menuDoc = CurrentUserUtils.setupContextMenuBtn(CurrentUserUtils.filterBtnDesc(ToTagName(hotKey), 'question'), filterIcons); + Doc.AddToFilterHotKeys(menuDoc); + } }; @computed get mainInnerContent() { @@ -941,13 +945,13 @@ export class MainView extends ObservableReactComponent<object> { closeFlyout = action(() => { SnappingManager.SetLastPressedBtn(''); this._panelContent = 'none'; - this._sidebarContent.proto = undefined; + this._sidebarContent && (this._sidebarContent.proto = undefined); this._leftMenuFlyoutWidth = 0; }); - remButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); + remButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && !!Doc.MyDockedBtns && Doc.RemoveDocFromList(Doc.MyDockedBtns!, 'data', doc), true); moveButtonDoc = (docs: Doc | Doc[], targetCol: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(docs) && addDocument(docs); - addButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true); + addButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && !!Doc.MyDockedBtns && Doc.AddDocToList(Doc.MyDockedBtns!, 'data', doc), true); buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts index d756830da..805dd9a49 100644 --- a/src/client/views/PinFuncs.ts +++ b/src/client/views/PinFuncs.ts @@ -70,13 +70,13 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) { pinProps.pinData.dataview || pinProps.pinData.poslayoutview || pinProps?.activeFrame !== undefined; - const fkey = Doc.LayoutFieldKey(targetDoc); + const fkey = Doc.LayoutDataKey(targetDoc); if (pinProps.pinData.dataview) { pinDoc.config_usePath = targetDoc[fkey + '_usePath']; pinDoc.config_data = Field.Copy(targetDoc[fkey]); } if (pinProps.pinData.dataannos) { - const fieldKey = '$' + Doc.LayoutFieldKey(targetDoc) + +'_annotations'; + const fieldKey = '$' + Doc.LayoutDataKey(targetDoc) + +'_annotations'; pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[fieldKey]).filter(doc => !doc.layout_unrendered)); } if (pinProps.pinData.inkable) { @@ -87,7 +87,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) { } if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop; if (pinProps.pinData.clippable) { - const fieldKey = Doc.LayoutFieldKey(targetDoc); + const fieldKey = Doc.LayoutDataKey(targetDoc); pinDoc.config_clipWidth = targetDoc[fieldKey + '_clipWidth']; } if (pinProps.pinData.datarange) { @@ -127,7 +127,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) { } if (pinProps.pinData.temporal) { pinDoc.config_clipStart = targetDoc._layout_currentTimecode; - const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1); + const duration = NumCast(pinDoc[`${Doc.LayoutDataKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1); pinDoc.config_clipEnd = NumCast(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration); } } diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 6778a6691..66936c64c 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -447,7 +447,7 @@ export class PropertiesButtons extends React.Component { }; render() { - const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)]; + const layoutField = this.selectedDoc?.[Doc.LayoutDataKey(this.selectedDoc)]; const isText = DocumentView.Selected().lastElement()?.ComponentView instanceof FormattedTextBox; const isInk = this.selectedDoc?.layout_isSvg; const isImage = layoutField instanceof ImageField; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 7fcb15afe..e7186c0e3 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -525,7 +525,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps let permission; if (this.layoutDocAcls) { if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; - else if (target.embedContainer) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer))[userKey]); + else if (DocCast(target.embedContainer)) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer)!)[userKey]); else permission = StrCast(Doc.GetProto(target)?.[userKey]); } else permission = StrCast(target[userKey]); individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user @@ -546,7 +546,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps if (this.layoutDocAcls) { if (target[DocAcl][groupKey]) { permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name; - } else if (target.embedContainer) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer))[groupKey]); + } else if (DocCast(target.embedContainer)) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer)!)[groupKey]); else permission = StrCast(Doc.GetProto(target)?.[groupKey]); } else permission = StrCast(target[groupKey]); groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false)); @@ -1921,7 +1921,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0; const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as DocumentType) ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as DocumentType) - : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type; + : PresBox.Instance.activeItem + ? PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type + : undefined; return ( <div className="propertiesView" style={{ width: this._props.width }}> <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}> diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index e8876f96f..9110e198f 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -129,7 +129,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & // prettier-ignore switch (property.split(':')[0]) { case StyleProp.TreeViewIcon: { - const img = ImageCast(doc?.icon ?? doc?.[doc ? Doc.LayoutFieldKey(doc) : ""]); + const img = ImageCast(doc?.icon ?? doc?.[doc ? Doc.LayoutDataKey(doc) : ""]); if (img) { const ext = extname(img.url.href); const url = doc?.icon ? img.url.href : img.url.href.replace(ext, '_s' + ext); @@ -164,7 +164,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & return undefined; case StyleProp.DocContents: return undefined; case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; - case StyleProp.Opacity: return componentView?.isUnstyledView?.() ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); + case StyleProp.Opacity: return componentView?.isUnstyledView?.() ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null) ?? null); case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], isCaption ? lightOrDark(backgroundCol()) : StrCast(Doc.UserDoc().fontColor, color())); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); @@ -248,13 +248,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & : 0; case StyleProp.BackgroundColor: { if (SnappingManager.LastPressedBtn === doc?.[Id]) return SnappingManager.userColor; // hack to indicate active menu panel item - const dataKey = doc ? Doc.LayoutFieldKey(doc) : ''; + const dataKey = doc ? Doc.LayoutDataKey(doc) : ''; const usePath = StrCast(doc?.[dataKey + '_usePath']); const alternate = usePath.includes(':hover') ? ( isHovering?.() ? '_' + usePath.replace(':hover','') : '') : usePath ? "_" +usePath:usePath; let docColor:Opt<string> = layoutDoc && StrCast(alternate ? layoutDoc['backgroundColor' + alternate]:undefined, - doc.rootDocument - ? StrCast(layoutDoc.backgroundColor, StrCast(DocCast(doc.rootDocument).backgroundColor)) // for nested templates: use template's color, then root doc's color + DocCast(doc.rootDocument) + ? StrCast(layoutDoc.backgroundColor, StrCast(DocCast(doc.rootDocument)!.backgroundColor)) // for nested templates: use template's color, then root doc's color : layoutDoc === doc ? StrCast(doc.backgroundColor) : StrCast(StrCast(Doc.GetT(layoutDoc, 'backgroundColor', 'string', true), StrCast(doc.backgroundColor, StrCast(layoutDoc.backgroundColor)) // otherwise, use expanded template coloor, then root doc's color, then template's inherited color @@ -262,7 +262,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & // prettier-ignore switch (layoutDoc?.type) { - case DocumentType.PRESELEMENT: docColor = docColor || ""; break; + case DocumentType.PRESSLIDE: docColor = docColor || ""; break; case DocumentType.PRES: docColor = docColor || 'transparent'; break; case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || SnappingManager.userBackgroundColor; break; case DocumentType.RTF: docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break; diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx index b4a1f53c5..fe76bce28 100644 --- a/src/client/views/StyleProviderQuiz.tsx +++ b/src/client/views/StyleProviderQuiz.tsx @@ -67,7 +67,7 @@ export namespace styleProviderQuiz { newCol.zIndex = 1000; newCol.forceActive = true; newCol.quiz = text; - newCol['$' + Doc.LayoutFieldKey(newCol) + '_transform'] = 'none'; + newCol['$' + Doc.LayoutDataKey(newCol) + '_transform'] = 'none'; Doc.AddDocToList(img.Document, 'quizBoxes', newCol); img.addDocument(newCol); // img._loading = false; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 79c4d8af1..40d4f108d 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -21,6 +21,7 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { IconTagBox } from './nodes/IconTagBox'; import { Id } from '../../fields/FieldSymbols'; import { StyleProp } from './StyleProp'; +import { Docs } from '../documents/Documents'; /** * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection. @@ -171,6 +172,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { const newEmbeddings = TagItem.allDocsWithTag(this._props.tag).map(doc => Doc.MakeEmbedding(doc)); // Create a new collection and set up configurations. + const emptyCol = DocCast(Doc.UserDoc().emptyCollection); const newCollection = ((doc: Doc) => { doc.$data = new List<Doc>(newEmbeddings); doc.$title = this._props.tag; @@ -182,7 +184,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { doc.layout_fitWidth = true; doc._layout_showTags = true; return doc; - })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); + })(emptyCol ? Doc.MakeCopy(emptyCol, true) : Docs.Create.FreeformDocument([], {})); newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection)); // Add the collection to the tag document's list of associated smart collections. @@ -342,7 +344,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { const tagsList = new Set<string>(StrListCast(this.View.dataDoc.tags)); const chatTagsList = new Set<string>(StrListCast(this.View.dataDoc.tags_chat)); const facesList = new Set<string>( - DocListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_annotations']) + DocListCast(this.View.dataDoc[Doc.LayoutDataKey(this.View.Document) + '_annotations']) .concat(this.View.Document) .filter(d => d.face) .map(doc => StrCast(DocCast(doc.face)?.title)) diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index cd2c7df1b..814e9a7a0 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -92,7 +92,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D private get children(): Doc[] { const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this._props.Doc.type) as unknown as DocumentType); if (annotatedDoc) { - return DocListCast(this._props.Doc[Doc.LayoutFieldKey(this._props.Doc) + '_annotations']); + return DocListCast(this._props.Doc[Doc.LayoutDataKey(this._props.Doc) + '_annotations']); } return DocListCast(this._props.Doc[this._props.fieldKey]); } diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 2d3f8cb0e..975dc52fe 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -77,7 +77,8 @@ export class CollectionCarouselView extends CollectionSubView() { focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => { const docs = DocListCast(this.Document[this.fieldKey]); if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) { - const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor)); + const annoOn = DocCast(anchor.annotationOn, anchor); + const newIndex = NumCast(anchor.config_carousel_index, (annoOn && docs.getIndex(annoOn)) ?? 0); options.didMove = newIndex !== this.layoutDoc._carousel_index; options.didMove && (this.layoutDoc._carousel_index = newIndex); } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 094e61db9..ea6259a32 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -473,11 +473,11 @@ export class CollectionDockingView extends CollectionSubView() { } return undefined; } - public static async TakeSnapshot(doc: Doc | undefined, clone = false) { + public static TakeSnapshot(doc: Doc | undefined, clone = false) { if (!doc) return undefined; let json = StrCast(doc.dockingConfig); if (clone) { - const cloned = await Doc.MakeClone(doc); + const cloned = Doc.MakeClone(doc); Array.from(cloned.map.entries()).forEach(entry => { json = json.replace(entry[0], entry[1][Id]); }); @@ -524,11 +524,11 @@ export class CollectionDockingView extends CollectionSubView() { this._flush = this._flush ?? UndoManager.StartBatch('tab movement'); const dashDoc = tab.DashDoc; if (dashDoc && ![DocumentType.PRES].includes(dashDoc.type) && !tab.contentItem.config.props.keyValue) { - Doc.AddDocToList(Doc.MyHeaderBar, 'data', dashDoc, undefined, undefined, true); + Doc.MyHeaderBar && Doc.AddDocToList(Doc.MyHeaderBar, 'data', dashDoc, undefined, undefined, true); // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed if (dashDoc.embedContainer === this.Document) dashDoc.embedContainer = undefined; if (!dashDoc.embedContainer) { - Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashDoc, undefined, true, true); + Doc.MyRecentlyClosed && Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashDoc, undefined, true, true); Doc.RemoveEmbedding(dashDoc, dashDoc); } } @@ -562,7 +562,7 @@ export class CollectionDockingView extends CollectionSubView() { _layout_fitWidth: true, title: `Untitled Tab ${NumCast(dashboard.$myPaneCount)}`, }); - Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true); + Doc.MyHeaderBar && Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true); inheritParentAcls(this.Document, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } @@ -579,7 +579,7 @@ export class CollectionDockingView extends CollectionSubView() { _freeform_backgroundGrid: true, title: `Untitled Tab ${NumCast(dashboard.$myPaneCount)}`, }); - Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true); + Doc.MyHeaderBar && Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true); inheritParentAcls(this.dataDoc, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } @@ -668,7 +668,5 @@ ScriptingGlobals.add( ); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(async function snapshotDashboard() { - const batch = UndoManager.StartBatch('snapshot'); - await CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard); - batch.end(); + undoable(() => CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard), 'snapshot dashboard'); }, 'creates a snapshot copy of a dashboard'); diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 173147f64..7f639a11e 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -321,11 +321,10 @@ export class CollectionNoteTakingView extends CollectionSubView() { return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); }; - // how to get the height of a document. Nothing special here. getDocHeight(d?: Doc) { if (!d || d.hidden) return 0; - const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); - const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument; + const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.()); + const childDataDoc = d.isTemplateDoc || d.isTemplateForField ? this._props.TemplateDataDocument : undefined; const maxHeight = (lim => (lim === 0 ? this._props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d)) ? NumCast(d._width) : 0); const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d)) ? NumCast(d._height) : 0); @@ -566,7 +565,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { @undoBatch remColumn = (value: SchemaHeaderField) => { - const colHdrData = Array.from(Cast(this.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); + const colHdrData = Array.from(Cast(this.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []); if (value) { const index = colHdrData.indexOf(value); index !== -1 && colHdrData.splice(index, 1); @@ -585,7 +584,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { } return undefined; }); - const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); + const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []); const newColWidth = 1 / (this.numGroupColumns + 1); columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)); value && this.resizeColumns(columnHeaders); @@ -642,7 +641,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); const headers = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); - headers.splice(headers.indexOf(leftHeader), 1, leftHeader[Copy]()); + headers?.splice(headers.indexOf(leftHeader), 1, leftHeader[Copy]()); }; // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 112510265..9155227dd 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -315,8 +315,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection e.stopPropagation?.(); const layoutFieldKey = StrCast(textBox.fieldKey); const newDoc = Doc.MakeCopy(textBox.Document, true); - const dataField = textBox.Document[Doc.LayoutFieldKey(newDoc)]; - newDoc['$' + Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + const dataField = textBox.Document[Doc.LayoutDataKey(newDoc)]; + newDoc['$' + Doc.LayoutDataKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; if (layoutFieldKey !== 'layout' && textBox.Document[layoutFieldKey] instanceof Doc) { newDoc[layoutFieldKey] = textBox.Document[layoutFieldKey]; } @@ -340,7 +340,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection isChildButtonContentActive = () => (this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined); @observable docRefs = new ObservableMap<Doc, DocumentView>(); - childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); + childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null) ?? null); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, trans: () => string, count: number) { @@ -408,7 +408,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }); getDocWidth = computedFn((d?: Doc) => () => { if (!d) return 0; - const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); + const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) { return Math.min(NumCast(d._width), maxWidth); @@ -418,8 +418,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition)); getDocHeight = computedFn((d?: Doc) => () => { if (!d || d.hidden) return 0; - const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); - const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument; + const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.()); + const childDataDoc = d.isTemplateDoc || d.isTemplateForField ? this._props.TemplateDataDocument : undefined; const maxHeight = (lim => (lim === 0 ? this._props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0); const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0); diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 66839ba7f..994669734 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -274,7 +274,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< const container = DocCast(this._props.Doc.rootDocument)?.[DocData] ? Doc.GetProto(this._props.Doc) : this._props.Doc; if (container.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, container); - return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + return Doc.AddDocToList(container, Doc.LayoutDataKey(container), created); } return this._props.addDocument?.(created) || false; } @@ -316,7 +316,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< ref={this._headerRef} style={{ marginTop: this._props.yMargin, - width: this._props.columnWidth + width: this._props.columnWidth, }}> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 375c0fe53..bc7d6f897 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import * as rp from 'request-promise'; import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils'; import CursorField from '../../../fields/CursorField'; -import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, expandedFieldName, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -132,16 +132,23 @@ export function CollectionSubView<X>() { hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] { - const { TemplateDataDocument } = this._props; - const validPairs = this.childDocs - .map(doc => Doc.GetLayoutDataDocPair(this.Document, !this._props.isAnnotationOverlay ? TemplateDataDocument : undefined, doc)) - .filter( - pair => - // filter out any documents that have a proto that we don't have permissions to - !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate)) - ) - .filter(pair => !this._filterFunc?.(pair.layout!)); - return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types + const lastEle = this.DocumentView?.(); + if (!lastEle?.IsInvalid(this.Document)) { + const rootTemplate = lastEle && Doc.LayoutDoc(lastEle.rootDoc).isTemplateDoc && Doc.LayoutDoc(lastEle.rootDoc); + const templateFieldKey = rootTemplate && + [expandedFieldName(rootTemplate), + ...this._props.docViewPath() + .filter(dv => dv.Document.isTemplateForField) + .map(dv => dv.Document.title), + ].join('_'); // prettier-ignore + return this.childDocs + .map(doc => Doc.GetLayoutDataDocPair(this.Document, !this._props.isAnnotationOverlay ? this._props.TemplateDataDocument : undefined, doc, templateFieldKey || "")) + .filter(pair => // filter out any documents that have a proto that we don't have permissions to + !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate))) + .filter(pair => !this._filterFunc?.(pair.layout!)) + .map(({ data, layout }) => ({ data: data!, layout: layout! })); // prettier-ignore + } + return []; } /** * This is the raw, stored list of children on a collection. If you modify this list, the database will be updated @@ -161,7 +168,7 @@ export function CollectionSubView<X>() { }; collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters); - collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []); + collectionRangeDocFilters = () => this._focusRangeFilters ?? StrListCast(this.Document._childFiltersByRanges); // child filters apply to the descendants of the documents in this collection childDocFilters = () => [...(this._props.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; // unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack @@ -178,7 +185,7 @@ export function CollectionSubView<X>() { rawdocs = [this.dataField]; } else if (Cast(this.dataField, listSpec(Doc), null)) { // otherwise, if the collection data is a list, then use it. - rawdocs = Cast(this.dataField, listSpec(Doc), null); + rawdocs = DocListCast(this.dataField); } else if (this.dataField) { // Finally, if it's not a doc or a list and the document is a template, we try to render the root doc. // For example, if an image doc is rendered with a slide template, the template will try to render the data field as a collection. @@ -206,7 +213,7 @@ export function CollectionSubView<X>() { let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0; - const fieldKey = Doc.LayoutFieldKey(d); + const fieldKey = Doc.LayoutDataKey(d); const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc)); const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey]; const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar']; @@ -219,7 +226,7 @@ export function CollectionSubView<X>() { newarray = []; // eslint-disable-next-line no-loop-func subDocs.forEach(t => { - const docFieldKey = Doc.LayoutFieldKey(t); + const docFieldKey = Doc.LayoutDataKey(t); const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc)); notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 568a08792..4348bc7dc 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -15,7 +15,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast, StrCast, toList } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast, toList } from '../../../fields/Types'; import { DocServer } from '../../DocServer'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -145,7 +145,7 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps select={emptyFunction} isSelected={returnFalse} dontRegisterView - fieldKey={Doc.LayoutFieldKey(this._props.doc)} + fieldKey={Doc.LayoutDataKey(this._props.doc)} addDocument={returnFalse} moveDocument={returnFalse} removeDocument={returnFalse} @@ -173,7 +173,7 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps ); }; render() { - return this._props.doc.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.doc)) || this._props.doc?._type_collection !== CollectionViewType.Freeform ? null : ( + return this._props.doc.layout !== CollectionView.LayoutString(Doc.LayoutDataKey(this._props.doc)) || this._props.doc?._type_collection !== CollectionViewType.Freeform ? null : ( <div className="miniMap-hidden"> <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SnappingManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} /> </div> @@ -207,10 +207,10 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { const docs = toList(docIn); const batch = UndoManager.StartBatch('Pin doc to pres trail'); - const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); + const curPres = Doc.ActivePresentation ?? (DocCast(Doc.UserDoc().emptyTrail) ? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail)!, true) : Docs.Create.PresDocument({})); if (!Doc.ActivePresentation) { - Doc.AddDocToList(Doc.MyTrails, 'data', curPres); + Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', curPres); Doc.ActivePresentation = curPres; } @@ -236,7 +236,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { pinDoc.treeView_FieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field pinDoc.treeView_ExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view pinDoc.treeView_HideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header - const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], null); + const duration = NumCast(doc[`${Doc.LayoutDataKey(pinDoc)}_duration`], null); if (pinProps.pinViewport) PinDocView(pinDoc, pinProps, anchorDoc ?? doc); if (!pinProps?.audioRange && duration !== undefined) { @@ -566,7 +566,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { return false; }; - getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame); + getCurrentFrame = () => NumCast(DocCast(PresBox.Instance.activeItem?.presentation_targetDoc)?._currentFrame); focusFunc = () => { if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 4dc937864..4beb75074 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -159,7 +159,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return this.Document[DocLayout]; } @computed get fieldKey() { - return StrCast(this.Document._treeView_FieldKey, Doc.LayoutFieldKey(this.Document)); + return StrCast(this.Document._treeView_FieldKey, Doc.LayoutDataKey(this.Document)); } @computed get childDocs() { return this.childDocList(this.fieldKey); @@ -186,7 +186,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { moving: boolean = false; @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { if (this.Document !== target && addDoc !== returnFalse) { - const canAdd1 = (this._props.parentTreeView as TreeView).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this._props.parentTreeView?.Document.data)) instanceof ComputedField); + const canAdd1 = (this._props.parentTreeView as TreeView).dropping || !(ComputedField.DisableCompute(() => FieldValue(this._props.parentTreeView?.Document.data)) instanceof ComputedField); // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse if (canAdd1 && this._props.removeDoc?.(doc) === true) { @@ -431,9 +431,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { localAdd = (docs: Doc | Doc[]): boolean => { const innerAdd = (doc: Doc): boolean => { - const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; + const dataIsComputed = ComputedField.DisableCompute(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer)); + dataIsComputed && DocCast(this.Document.embedContainer) && Doc.SetContainer(doc, DocCast(this.Document.embedContainer)!); return added; }; return toList(docs).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -511,9 +511,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const moveDoc = (docs: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(docs, target, addDoc); const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => { const innerAdd = (iDoc: Doc) => { - const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; + const dataIsComputed = ComputedField.DisableCompute(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, iDoc, addBefore, before, false, true); - dataIsComputed && Doc.SetContainer(iDoc, DocCast(this.Document.embedContainer)); + dataIsComputed && DocCast(this.Document.embedContainer) && Doc.SetContainer(iDoc, DocCast(this.Document.embedContainer)!); return added; }; return toList(docs).reduce((flg, iDoc) => flg && innerAdd(iDoc), true as boolean); @@ -633,7 +633,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { d.zIndex = i; }); } - const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; + const dataIsComputed = ComputedField.DisableCompute(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false); !dataIsComputed && added && Doc.SetContainer(doc, this.Document); @@ -883,8 +883,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { ]; }; childContextMenuItems = () => { - const customScripts = Cast(this.Document.childContextMenuScripts, listSpec(ScriptField), []); - const customFilters = Cast(this.Document.childContextMenuFilters, listSpec(ScriptField), []); + const customScripts = Cast(this.Document.childContextMenuScripts, listSpec(ScriptField), [])!; + const customFilters = Cast(this.Document.childContextMenuFilters, listSpec(ScriptField), [])!; const icons = StrListCast(this.Document.childContextMenuIcons); return StrListCast(this.Document.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); }; @@ -1296,7 +1296,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => { if (parent instanceof TreeView && parent._props.treeView.fileSysMode && !newParent.isFolder) return; - const fieldKey = Doc.LayoutFieldKey(newParent); + const fieldKey = Doc.LayoutDataKey(newParent); if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) { remove(child); DocumentView.SetSelectOnLoad(child); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 626538976..842293358 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -275,7 +275,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); elementFunc = () => this._layoutElements; viewTransition = () => (this._panZoomTransition ? '' + this._panZoomTransition : undefined); - panZoomTransition = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))); + panZoomTransition = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : (Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null) ?? null) ?? '')); fitContentOnce = () => { const { cx, cy, scale } = this.contentBounds(); // prettier-ignore this.layoutDoc._freeform_panX = cx; @@ -388,7 +388,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (options.easeFunc) this.setPresEaseFunc(options.easeFunc); if (this._lightboxDoc) return undefined; if (options.pointFocus) return this.focusOnPoint(options); - const anchorInCollection = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor); + const anchorInCollection = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutDataKey(this.Document)]).includes(anchor); const anchorInChildViews = this.childLayoutPairs.map(pair => pair.layout).includes(anchor); if (!anchorInCollection && !anchorInChildViews) { return undefined; @@ -1308,7 +1308,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection SnappingManager.TriggerUserPanned(); if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return; e.stopPropagation(); - const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight); + const docHeight = NumCast(this.Document[Doc.LayoutDataKey(this.Document) + '_nativeHeight'], this.nativeHeight); const scrollable = this.isAnnotationOverlay && NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling + 1e-4; switch ( !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey ?// @@ -1489,12 +1489,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection */ createTextDocCopy = undoable((textBox: FormattedTextBox, below: boolean) => { const textDoc = DocCast(textBox.Document); - const newDoc = Doc.MakeCopy(textDoc, true); - newDoc['$' + Doc.LayoutFieldKey(newDoc)] = undefined; // the copy should not copy the text contents of it source, just the render style - newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10); - newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0); - DocumentView.SetSelectOnLoad(newDoc); - return this.addDocument?.(newDoc); + if (textDoc) { + const newDoc = Doc.MakeCopy(textDoc, true); + newDoc['$' + Doc.LayoutDataKey(newDoc)] = undefined; // the copy should not copy the text contents of it source, just the render style + newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10); + newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0); + DocumentView.SetSelectOnLoad(newDoc); + return this.addDocument?.(newDoc); + } + return false; }, 'copied text note'); onKey = (e: KeyboardEvent, textBox: FormattedTextBox) => { @@ -1741,7 +1744,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (addAsAnnotation) { if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor); + Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), [])?.push(anchor); } else { this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]); } @@ -1985,7 +1988,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection optionItems.push({ description: 'Use Background Color as Default', event: () => { - Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor); + DocCast(Doc.UserDoc().emptyCollection) && (DocCast(Doc.UserDoc().emptyCollection)!.backgroundColor = StrCast(this.layoutDoc.backgroundColor)); }, icon: 'palette', }); @@ -2085,7 +2088,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection lightboxPanelHeight = () => Math.max(0, this._props.PanelHeight() - 30); lightboxScreenToLocal = () => this.ScreenToLocalBoxXf().translate(-15, -15); onPassiveWheel = (e: WheelEvent) => { - const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight); + const docHeight = NumCast(this.Document[Doc.LayoutDataKey(this.Document) + '_nativeHeight'], this.nativeHeight); const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling; this._props.isSelected() && !scrollable && e.preventDefault(); }; @@ -2108,7 +2111,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection </div> ); } - transitionFunc = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms ${this._presEaseFunc}` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))); + transitionFunc = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms ${this._presEaseFunc}` : (Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null) ?? null) ?? '')); get pannableContents() { this.incrementalRender(); // needs to happen synchronously or freshly typed text documents will flash and miss their first characters return ( @@ -2385,7 +2388,7 @@ ScriptingGlobals.add(function datavizFromSchema() { const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key !== 'text'); if (!keys) return; - const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]); + const children = DocListCast(view.Document[Doc.LayoutDataKey(view.Document)]); const csvRows = []; csvRows.push(keys.join(',')); for (let i = 0; i < children.length; i++) { diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index c7aa410c6..72485aa86 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -187,7 +187,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); })}> {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { - const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]) ?? ['-missing-', '.png']; + const [name, type] = ImageCastToNameType(doc[Doc.LayoutDataKey(doc)]) ?? ['-missing-', '.png']; return ( <div className="image-wrapper" diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 7c8ccb92d..ff9fb14e7 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -165,7 +165,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { const imageInfos = this._selectedImages.map(async doc => { if (!doc.$tags_chat) { - const url = ImageCastWithSuffix(doc[Doc.LayoutFieldKey(doc)], '_o') ?? ''; + const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? ''; return imageUrlToBase64(url).then(hrefBase64 => !hrefBase64 ? undefined : gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels => @@ -310,7 +310,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { {this._displayImageInformation ? ( <div className="image-information-list"> {this._selectedImages.map(doc => { - const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]); + const [name, type] = ImageCastToNameType(doc[Doc.LayoutDataKey(doc)]); return ( <div className="image-information" style={{ borderColor: SettingsManager.userColor }} key={Utils.GenerateGuid()}> <img diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5fbf72f39..3cc7c0f2d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -8,7 +8,7 @@ import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSy import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; import { DocUtils } from '../../../documents/DocUtils'; @@ -376,7 +376,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps doc.$title = makeGroup ? 'grouping' : 'nested freeform'; doc._freeform_panX = doc._freeform_panY = 0; return doc; - })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); + })(DocCast(Doc.UserDoc().emptyCollection) ? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyCollection)!, true) : Docs.Create.FreeformDocument([], {})); newCollection.isSystem = undefined; newCollection._width = bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children newCollection._height = bounds.height || 1; @@ -437,7 +437,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps */ @undoBatch classifyImages = async () => { - const groupButton = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImageGrouper); + const groupButton = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyImageGrouper); if (groupButton) { this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG); ImageLabelBoxData.Instance.setData(this._selectedDocs); diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 80116dd2f..8d3947653 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -102,7 +102,7 @@ export class CollectionLinearView extends CollectionSubView() { getLinkUI = () => !DocumentLinksButton.StartLink ? null : ( - <span className="bottomPopup-background" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}> + <span key="-link-ui-" className="bottomPopup-background" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}> <span className="bottomPopup-text"> Creating link from:{' '} <b> @@ -126,7 +126,7 @@ export class CollectionLinearView extends CollectionSubView() { ); getCurrentlyPlayingUI = () => !DocumentView.CurrentlyPlaying?.length ? null : ( - <span className="bottomPopup-background"> + <span key="-currently-playing-" className="bottomPopup-background"> <span className="bottomPopup-text"> Currently playing: {DocumentView.CurrentlyPlaying.map((clip, i) => ( diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 5803acca0..82ca96839 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1193,7 +1193,7 @@ export class CollectionSchemaView extends CollectionSubView() { let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0; - const fieldKey = Doc.LayoutFieldKey(d); + const fieldKey = Doc.LayoutDataKey(d); const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc)); const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey]; const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar']; @@ -1206,7 +1206,7 @@ export class CollectionSchemaView extends CollectionSubView() { newarray = []; // eslint-disable-next-line no-loop-func subDocs.forEach(t => { - const docFieldKey = Doc.LayoutFieldKey(t); + const docFieldKey = Doc.LayoutDataKey(t); const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc)); notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc)); diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 173984dc7..8e1edc1ee 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -326,7 +326,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro const field = Cast(this._props.Doc[this._props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this._props.Doc[this._props.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images const altpaths = alts - .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) + .map(doc => Cast(doc[Doc.LayoutDataKey(doc)], ImageField, null)?.url) .filter(url => url) .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 32afb3d3d..f4beb1004 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -76,7 +76,7 @@ ScriptingGlobals.add(function setBorderColor(color?: string, checkResult?: boole const selView = selectedViews.lastElement(); const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed - return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultBorder(); + return (contentFrameNumber !== undefined && CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey]) || defaultBorder(); } setDefaultBorder(color ?? 'transparent'); selectedViews.forEach(dv => { @@ -87,7 +87,7 @@ ScriptingGlobals.add(function setBorderColor(color?: string, checkResult?: boole obj[fieldKey] = color; CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj); } else { - const dataKey = Doc.LayoutFieldKey(dv.Document); + const dataKey = Doc.LayoutDataKey(dv.Document); const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', ''); dv.layoutDoc[fieldKey + alternate] = undefined; dv.dataDoc[fieldKey + alternate] = color; @@ -133,7 +133,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj); } else { const colorDoc = dv.isTemplateForField ? dv.layoutDoc : dv.dataDoc; // assigning to a template's compoment field should not assign to the data doc - const dataKey = Doc.LayoutFieldKey(colorDoc); + const dataKey = Doc.LayoutDataKey(colorDoc); const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', ''); colorDoc[fieldKey + alternate] = color; } @@ -158,7 +158,7 @@ ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { if (checkResult) { - return DocumentView.Selected().length ? StrCast(DocumentView.SelectedDocs().lastElement().layout_headingColor) : Doc.SharingDoc().headingColor; + return DocumentView.Selected().length ? StrCast(DocumentView.SelectedDocs().lastElement().layout_headingColor) : Doc.SharingDoc()?.headingColor; } if (DocumentView.Selected().length) { DocumentView.SelectedDocs().forEach(doc => { @@ -166,8 +166,11 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole doc.layout_showTitle = color === 'transparent' ? undefined : StrCast(doc.layout_showTitle, 'title'); }); } else { - Doc.SharingDoc().headingColor = undefined; - Doc.GetProto(Doc.SharingDoc()).headingColor = color === 'transparent' ? undefined : color; + const sharing = Doc.SharingDoc(); + if (sharing) { + sharing.headingColor = undefined; + Doc.GetProto(sharing).headingColor = color === 'transparent' ? undefined : color; + } Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'title'); } return undefined; @@ -234,30 +237,30 @@ ScriptingGlobals.add(function showFreeform( setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; }, }], ['time', { - checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "time", - setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutDataKey(doc)+"_sort"]) === "time", + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutDataKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutDataKey(doc)+"_sort"] = '' : doc[Doc.LayoutDataKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore }], ['docType', { - checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "type", - setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutDataKey(doc)+"_sort"]) === "type", + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutDataKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutDataKey(doc)+"_sort"] = '' : doc[Doc.LayoutDataKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore }], ['color', { - checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "color", - setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutFieldKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutDataKey(doc)+"_sort"]) === "color", + setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutDataKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutDataKey(doc)+"_sort"] = '' : doc[Doc.LayoutDataKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore }], ['tag', { - checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag", - setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutDataKey(doc)+"_sort"]) === "tag", + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutDataKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutDataKey(doc)+"_sort"] = '' : doc[Doc.LayoutDataKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore }], ['reverse', { - checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_reverse"]), - setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"] = !doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"]; }, + checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutDataKey(doc)+"_sort_reverse"]), + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutDataKey(doc)+"_sort_reverse"] = !doc[Doc.LayoutDataKey(doc)+"_sort_reverse"]; }, }], ['toggle-chat', { checkResult: (doc: Doc) => SnappingManager.ChatVisible, setDoc: (doc: Doc, dv: DocumentView) => { if (SnappingManager.ChatVisible){ - doc[Doc.LayoutFieldKey(doc)+"_sort"] = ''; + doc[Doc.LayoutDataKey(doc)+"_sort"] = ''; SnappingManager.SetChatVisible(false); } else { SnappingManager.SetChatVisible(true); diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx index b060fc0b6..87cef01d2 100644 --- a/src/client/views/newlightbox/NewLightboxView.tsx +++ b/src/client/views/newlightbox/NewLightboxView.tsx @@ -77,7 +77,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { NewLightboxView.SetNewLightboxDoc( doc, undefined, - [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort( + [...DocListCast(doc[Doc.LayoutDataKey(doc)]), ...DocListCast(doc[Doc.LayoutDataKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort( (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow) ), layoutTemplate diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index e0d59cc9d..d4c512342 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -396,7 +396,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { returnFalse, action(() => { const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - const textField = Doc.LayoutFieldKey(newDoc); + const textField = Doc.LayoutDataKey(newDoc); const newDocData = newDoc[DocData]; newDocData[`${textField}_recordingSource`] = this.dataDoc; newDocData[`${textField}_recordingStart`] = ComputedField.MakeFunction(`this.${textField}_recordingSource.${this.fieldKey}_recordingStart`); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 53a3d3631..940c4cb99 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -71,7 +71,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames public static animStringFields = ['backgroundColor', 'borderColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames - public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames + public static animDataFields = (doc: Doc) => (Doc.LayoutDataKey(doc) ? [Doc.LayoutDataKey(doc)] : []); // fields that are configured to be animatable using animation frames public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined { return dv?._props.reactParent instanceof CollectionFreeFormDocumentView ? dv._props.reactParent : undefined; } @@ -187,7 +187,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } public static gotoKeyFrame(doc: Doc, newFrame: number) { if (doc) { - const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); + const childDocs = DocListCast(doc[Doc.LayoutDataKey(doc)]); const currentFrame = Cast(doc._currentFrame, 'number', null); if (currentFrame === undefined) { doc._currentFrame = 0; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 117eb05f8..c80e8ebc1 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -352,7 +352,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); if (!getFrom?.schema_columnKeys) return undefined; const keys = StrListCast(getFrom?.schema_columnKeys).filter(key => key !== 'text'); - const children = DocListCast(getFrom?.[Doc.LayoutFieldKey(getFrom)]); + const children = DocListCast(getFrom?.[Doc.LayoutDataKey(getFrom)]); const current: { [key: string]: string }[] = []; children .filter(child => child) diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 6f004bed3..504c1491e 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -188,8 +188,8 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte this._props.LayoutTemplate?.() || (this._props.LayoutTemplateString && this._props.Document) || (this._props.layoutFieldKey && StrCast(this._props.Document[this._props.layoutFieldKey]) && this._props.Document) || - Doc.Layout(this._props.Document, DocCast(this._props.Document[this._props.layoutFieldKey])); - return Doc.expandTemplateLayout(template, this._props.Document); + Doc.LayoutDoc(this._props.Document, DocCast(this._props.Document[this._props.layoutFieldKey])); + return Doc.expandTemplateLayout(template, this._props.Document, this._props.layoutFieldKey); } CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d88d8c44d..521934152 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -169,9 +169,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field } componentDidMount() { - runInAction(() => { - this._mounted = true; - }); + runInAction(() => (this._mounted = true)); this.setupHandlers(); this._disposers.contentActive = reaction( () => @@ -183,16 +181,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() ? true : undefined, - active => { - this._isContentActive = active; - }, + active => (this._isContentActive = active), { fireImmediately: true } ); this._disposers.pointerevents = reaction( () => this.style(this.Document, StyleProp.PointerEvents) as Property.PointerEvents | undefined, - pointerevents => { - this._pointerEvents = pointerevents; - }, + pointerevents => (this._pointerEvents = pointerevents), { fireImmediately: true } ); } @@ -305,7 +299,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field }; const clickFunc = this.onClickFunc?.()?.script ? () => (this.onClickFunc?.()?.script.run(scriptProps, console.log).result as Opt<{ select: boolean }>)?.select && this._props.select(false) : undefined; if (!clickFunc) { - // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + // onDragStart implies a button doc that we don't want to select when clicking. if (this.layoutDoc.onDragStart && !(e.ctrlKey || e.button > 0)) stopPropagate = false; preventDefault = false; } @@ -502,7 +496,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field const items = this._props.styleProvider?.(this.Document, this._props, StyleProp.ContextMenuItems) as ContextMenuProps[]; items?.forEach(item => ContextMenu.Instance.addItem(item)); - const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []); + const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), [])!; StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); @@ -536,9 +530,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field zorderItems.push({ description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', event: undoable( - action(() => { - this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged; - }), + action(() => (this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged)), 'set zIndex drag' ), icon: 'hand-point-up', @@ -663,7 +655,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field .scale(this.viewingAiEditor() ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1); onClickFunc = () => (this.disableClickScriptFunc ? undefined : this.onClickHdlr); setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore - setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore + setContentView = action((view: ViewBoxInterface<FieldViewProps>) => (this._componentView = view)); isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -763,6 +755,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field } widgetOverlayFunc = () => (this.widgetDecorations ? this.widgetOverlay : null); viewingAiEditor = () => (this._props.showAIEditor && this._componentView?.componentAIView?.() !== undefined ? this.aiEditor : null); + _contentsRef = React.createRef<DocumentContentsView>(); @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; @@ -778,6 +771,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field }}> <DocumentContentsView {...this._props} + ref={this._contentsRef} layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')} pointerEvents={this.contentPointerEvents} setContentViewBox={this.setContentView} @@ -812,8 +806,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); fieldsDropdown = (placeholder: string) => ( <div - ref={r => { r && runInAction(() => (this._titleDropDownInnerWidth = DivWidth(r)));}} // prettier-ignore - onPointerDown={action(() => { this._changingTitleField = true; })} // prettier-ignore + ref={action((r:HTMLDivElement|null) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} // prettier-ignore + onPointerDown={action(() => (this._changingTitleField = true))} style={{ width: 'max-content', background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> <FieldsDropdown Doc={this.Document} @@ -826,7 +820,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field } this._changingTitleField = false; })} - menuClose={action(() => { this._changingTitleField = false; })} // prettier-ignore + menuClose={action(() => (this._changingTitleField = false))} /> </div> ); @@ -842,7 +836,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field const background = StrCast( this.layoutDoc.layout_headingColor, // StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, - StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor) + StrCast(Doc.SharingDoc()?.headingColor, SnappingManager.userBackgroundColor) // ) ); const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0; @@ -1028,7 +1022,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field root: Doc ) { const effectDirection = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection; - const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)); + const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null) ?? null); const effectProps = { left: effectDirection === PresEffectDirection.Left, right: effectDirection === PresEffectDirection.Right, @@ -1143,7 +1137,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public static GetDocImage(doc?: Doc) { return DocumentView.getDocumentView(doc) ?.ComponentView?.updateIcon?.() - .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)]))); + .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutDataKey(doc!)]))); } public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore @@ -1181,6 +1175,24 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @observable public TagPanelHeight = 0; @observable public TagPanelEditing = false; + /** + * Tests whether the component Doc being rendered matches the Doc that this view thinks its rendering. + * When switching the layout_fieldKey, component views may react before the internal DocumentView has re-rendered. + * If this happens, the component view may try to write invalid data, such as when expanding a template (which + * depends on the DocumentView being in synch with the subcomponents). + * @param renderDoc sub-component Doc being rendered + * @returns boolean whether sub-component Doc is in synch with the layoutDoc that this view thinks its rendering + */ + IsInvalid = (renderDoc?: Doc): boolean => { + const docContents = this._docViewInternal?._contentsRef.current; + return !( + (!renderDoc || + (docContents?.layoutDoc === renderDoc && // + !this.docViewPath().some(dv => dv.IsInvalid()))) && + docContents?._props.layoutFieldKey === this.Document.layout_fieldKey + ); + }; + @computed get showTags() { return this.Document._layout_showTags || this._props.showTags; } @@ -1250,7 +1262,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this); } - public set IsSelected(val) { runInAction(() => { this._selected = val; }); } // prettier-ignore + public set IsSelected(val) { runInAction(() => (this._selected = val)) } // prettier-ignore public get IsSelected() { return this._selected; } // prettier-ignore public get IsContentActive(){ return this._docViewInternal?.isContentActive(); } // prettier-ignore public get topMost() { return this._props.renderDepth === 0; } // prettier-ignore @@ -1262,7 +1274,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public get HasAIEditor() { return !!this._docViewInternal?._componentView?.componentAIView?.(); } // prettier-ignore get LayoutFieldKey() { - return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString); + return Doc.LayoutDataKey(this.Document, this._props.LayoutTemplateString); } @computed get layout_fitWidth() { @@ -1316,10 +1328,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight()); - public iconify(finished?: () => void, animateTime?: number) { + public iconify = action((finished?: () => void, animateTime?: number) => { this.ComponentView?.updateIcon?.(); const animTime = this._docViewInternal?.animateScaleTime(); - runInAction(() => { this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); }); // prettier-ignore + this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); const finalFinished = action(() => { finished?.(); this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime); @@ -1329,12 +1341,12 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this.switchViews(true, 'icon', finalFinished); if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') this.Document.deiconifyLayout = layoutFieldKey.replace('layout_', ''); } else { - const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); + const deiconifyLayout = StrCast(this.Document.deiconifyLayout); this.switchViews(!!deiconifyLayout, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } - } + }); public playAnnotation = () => { const audioAnnoState = this.Document._audioAnnoState ?? AudioAnnoState.stopped; @@ -1349,7 +1361,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => { this.Document._audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore + onend: action(() => (this.Document._audioAnnoState = AudioAnnoState.stopped)), }); this.Document._audioAnnoState = AudioAnnoState.playing; break; @@ -1405,14 +1417,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { tempDoc = view.Document; MakeTemplate(tempDoc); Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); - Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); - tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc); + Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); + DocCast(Doc.UserDoc().template_user) && tempDoc && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', tempDoc); } else { - tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]); - if (!tempDoc) { - tempDoc = view.Document; - while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto); - } + tempDoc = DocCast(Doc.LayoutField(view.Document)); } } Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; @@ -1434,10 +1442,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(!!defaultLayout, defaultLayout, undefined, true); else this.switchViews(true, detailLayoutKeySuffix, undefined, true); }; - public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + public switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { const batch = UndoManager.StartBatch('switchView:' + view); // shrink doc first.. - runInAction(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); }); // prettier-ignore + this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); setTimeout( action(() => { if (useExistingLayout && custom && this.Document['layout_' + view]) { @@ -1457,7 +1465,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { }), Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) ); - }; + }); /** * @returns a hierarchy path through the nested DocumentViews that display this view. The last element of the path is this view. */ @@ -1510,7 +1518,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ref={r => { const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition if (r && val !== this._enableHtmlOverlayTransitions) { - setTimeout(action(() => { this._enableHtmlOverlayTransitions = val; })); // prettier-ignore + setTimeout(action(() => (this._enableHtmlOverlayTransitions = val))); } }} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}> @@ -1537,12 +1545,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { <div id={this.ViewGuid} className="contentFittingDocumentView" - onPointerEnter={action(() => { - this._isHovering = true; - })} - onPointerLeave={action(() => { - this._isHovering = false; - })}> + onPointerEnter={action(() => (this._isHovering = true))} // + onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1570,9 +1574,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { fitWidth={this.layout_fitWidthFunc} ScreenToLocalTransform={this.screenToContentsTransform} focus={this._props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => { - r && (this._docViewInternal = r); - })} + ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))} /> {this.htmlOverlay()} {this.ComponentView?.infoUI?.()} @@ -1616,7 +1618,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) { DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document)); } else { - const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); + const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc))!; const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; options.toggleTarget = undefined; DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ad6d93d43..de49f502f 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -80,7 +80,7 @@ export interface FieldViewSharedProps { setTitleFocus?: () => void; focus: FocusFuncType; onClickScript?: () => ScriptField | undefined; - onDoubleClickScript?: () => ScriptField; + onDoubleClickScript?: () => ScriptField | undefined; onPointerDownScript?: () => ScriptField; onPointerUpScript?: () => ScriptField; onKey?: (e: KeyboardEvent, textBox: FormattedTextBox) => boolean | undefined; diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx index e3924eca7..0bbd6a0d3 100644 --- a/src/client/views/nodes/IconTagBox.tsx +++ b/src/client/views/nodes/IconTagBox.tsx @@ -108,7 +108,7 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> { </Tooltip> )); // prettier-ignore - const audioannos = StrListCast(this.View.Document[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']); + const audioannos = StrListCast(this.View.Document[Doc.LayoutDataKey(this.View.Document) + '_audioAnnotations_text']); return !buttons.length && !audioannos.length ? null : ( <div className="card-button-container" style={{ fontSize: '50px' }}> {audioannos.length ? this.renderAudioButtons(this.View, audioannos.lastElement()) : null} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2b9a78596..0c475b7bb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -216,7 +216,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { this._regenerateLoading = true; const drag = de.complete.docDragData.draggedDocuments.lastElement(); - const dragField = drag[Doc.LayoutFieldKey(drag)]; + const dragField = drag[Doc.LayoutDataKey(drag)]; const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); @@ -224,7 +224,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetField = Doc.LayoutDataKey(layoutDoc); const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { added = true; @@ -264,7 +264,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth; - if (!RTFCast(doc[Doc.LayoutFieldKey(doc)])) { + if (!RTFCast(doc[Doc.LayoutDataKey(doc)])) { doc.width = (NumCast(doc.width) / oldnativeWidth) * newnativeWidth; doc.height = (NumCast(doc.height) / oldnativeWidth) * newnativeWidth; } @@ -351,9 +351,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { event: () => { Networking.PostToServer('/queryFireflyImageText', { file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + const ext = file ? extname(file) : ''; + return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), }).then(text => alert(text)); }, icon: 'expand-arrows-alt', @@ -364,9 +364,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Networking.PostToServer('/expandImage', { prompt: 'sunny skies', file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + const ext = file ? extname(file) : ''; + return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), }).then(res => { const info = res as Upload.ImageInformation; const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); @@ -510,7 +510,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="imageBox-regenerateDropTarget" ref={this._regenerateIconRef} - onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} + onClick={() => DocCast(this.Document.ai_firefly_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs)!, { openLocation: OpenWhere.addRight })} style={{ display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', transform: `scale(${this.uiBtnScaling})`, @@ -532,7 +532,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = alts - ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl) : defaultUrl)) + ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutDataKey(doc)])?.url ?? defaultUrl) : defaultUrl)) .filter(url => url) .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; @@ -664,7 +664,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const url = firstImg.pathname; const imgField = new ImageField(url); this._prevImgs.length === 0 && - this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); + this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field?.url.pathname ?? '' }); this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url }); this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); this.dataDoc.ai_firefly_prompt = firstImg.prompt; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index be897b3f3..706607fe1 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -96,7 +96,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { const { script, type, onDelegate } = kvpScript; const chooseDelegate = forceOnDelegate || onDelegate || keyIn.startsWith('_'); const key = chooseDelegate && keyIn.startsWith('$') ? keyIn.slice(1) : keyIn; - const target = chooseDelegate ? doc : DocCast(doc.proto, doc); + const target = chooseDelegate ? doc : DocCast(doc.proto, doc)!; let field: FieldType | undefined; switch (type) { case 'computed': field = new ComputedField(script); break; // prettier-ignore @@ -261,15 +261,15 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.Document).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${this.Document.title}`, _chromeHidden: true }); rows.forEach(row => { - const field = this.createFieldView(DocCast(this.Document), row); + const field = this.createFieldView(this.Document, row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); }); return parent; } - return rows.length ? this.createFieldView(DocCast(this.Document), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(this.Document, rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index c9e0aea5a..d8f968a40 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -61,7 +61,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { }; render() { - let doc = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; + let doc: Doc | undefined = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; const layoutField = doc !== this._props.doc; const key = layoutField ? this._props.keyName.replace(/^_/, '') : this._props.keyName; let protoCount = 0; @@ -85,7 +85,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { style={hover} className="keyValuePair-td-key-delete" onClick={undoable(() => { - delete (Object.keys(doc).indexOf(key) !== -1 ? doc : DocCast(this._props.doc.proto))[key]; + doc && delete (Object.keys(doc).indexOf(key) !== -1 ? doc : DocCast(this._props.doc.proto)!)[key]; }, 'set key value')}> X </button> diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 36d260fb9..83c44c80f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -210,7 +210,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, () => { - if (!(ComputedField.WithoutComputed(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { + if (!(ComputedField.DisableCompute(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { this.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } this.layoutDoc[this.SidebarKey + '_freeform_scale'] = 1; diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 53783e8a3..25c708da8 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -98,7 +98,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { }); screengrabber.overlayX = 70; // was -400 screengrabber.overlayY = 590; // was 0 - screengrabber['$' + Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + screengrabber['$' + Doc.LayoutDataKey(screengrabber) + '_trackScreen'] = true; Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay DocumentView.addViewRenderedCb(screengrabber, docView => { RecordingBox.screengrabber = docView.ComponentView as RecordingBox; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 999f9c1cd..4f02d68d6 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -284,7 +284,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() setupDictation = () => { if (this.dataDoc[this.fieldKey + '_dictation']) return; const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - const textField = Doc.LayoutFieldKey(dictationText); + const textField = Doc.LayoutDataKey(dictationText); dictationText._layout_autoHeight = false; const dictationTextProto = dictationText[DocData]; dictationTextProto[`${textField}_recordingSource`] = this.dataDoc; diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 8da422039..38a43e8d4 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,14 +1,12 @@ -/* eslint-disable react/button-has-type */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { returnAlways, returnEmptyString } from '../../../ClientUtils'; -import { Doc } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -26,10 +24,8 @@ import './ScriptingBox.scss'; import * as ts from 'typescript'; import { FieldType } from '../../../fields/ObjectField'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const getCaretCoordinates = require('textarea-caret'); -// eslint-disable-next-line @typescript-eslint/no-var-requires const ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; @observer @@ -105,7 +101,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.dataDoc[this.fieldKey + '-functionDescription'] = value; } @computed({ keepAlive: true }) get compileParams() { - return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); + return StrListCast(this.dataDoc[this.fieldKey + '-params']); } set compileParams(value) { this.dataDoc[this.fieldKey + '-params'] = new List<string>(value); @@ -426,7 +422,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() onChange={e => this.viewChanged(e, parameter)} value={typeof this.dataDoc[parameter] === 'string' ? 'S' + StrCast(this.dataDoc[parameter]) : typeof this.dataDoc[parameter] === 'number' ? 'N' + NumCast(this.dataDoc[parameter]) : 'B' + BoolCast(this.dataDoc[parameter])}> {types.map((type, i) => ( - // eslint-disable-next-line react/no-array-index-key <option key={i} className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}> {' '} {type.toString()}{' '} @@ -680,7 +675,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const definedParameters = !this.compileParams.length ? null : ( <div className="scriptingBox-plist" style={{ width: '30%' }}> {this.compileParams.map((parameter, i) => ( - // eslint-disable-next-line react/no-array-index-key <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <EditableView display="block" @@ -760,7 +754,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {!this.compileParams.length || !this.paramsNames ? null : ( <div className="scriptingBox-plist"> {this.paramsNames.map((parameter: string, i: number) => ( - // eslint-disable-next-line react/no-array-index-key <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}> <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index bad4f5b13..5603786f0 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -6,7 +6,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import * as WebRequest from 'web-request'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivHeight, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; -import { Doc, DocListCast, Field, FieldType, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; @@ -645,8 +645,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; forward = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string'), []); - const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = StrListCast(this.dataDoc[this.fieldKey + '_future']); + const history = StrListCast(this.dataDoc[this.fieldKey + '_history']); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { @@ -672,23 +672,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; back = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string')); - const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = StrListCast(this.dataDoc[this.fieldKey + '_future']); + const history = StrListCast(this.dataDoc[this.fieldKey + '_history']); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); + if (!future.length) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); else this.dataDoc[this.fieldKey + '_future'] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout( - action(() => { - this._webUrl = this._url; - }) - ); + setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; } diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index b023b1de6..528bcd05a 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -733,7 +733,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const x2 = parseFloat(values[2]) * Doc.NativeWidth(doc); const y2 = parseFloat(values[3]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc); - const annotationKey = '$' + Doc.LayoutFieldKey(doc) + '_annotations'; + const annotationKey = '$' + Doc.LayoutDataKey(doc) + '_annotations'; const existingDoc = DocListCast(doc[annotationKey]).find(d => d.citation_id === citation.citation_id); const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index cfb6dfa12..dc23a695d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -214,9 +214,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : this.rootDoc; - if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc; - const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); + if (!pinProps && this.EditorView?.state.selection.empty) return this.rootDoc; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc?.title), annotationOn: this.rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); @@ -245,7 +244,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB anchor.followLinkAudio = true; let stopFunc: () => void = emptyFunction; target.$mediaState = mediaState.Recording; - DictationManager.recordAudioAnnotation(target, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + DictationManager.recordAudioAnnotation(target, Doc.LayoutDataKey(target), stop => { stopFunc = stop }); // prettier-ignore const reactionDisposer = reaction( () => target.mediaState, @@ -427,8 +426,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const oldAutoLinks = Doc.Links(this.Document).filter( link => ((!Doc.isTemplateForField(this.Document) && - (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && - (!Doc.isTemplateForField(DocCast(link.link_anchor_2)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore @@ -450,18 +449,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB updateTitle = () => { const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( - !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing + !this._props.dontRegisterView && // only update the title if the data document's data field is changing title.startsWith('-') && this.EditorView && !this.dataDoc.title_custom && - (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') + (Doc.LayoutDataKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { let node = this.EditorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; const prefix = '-'; - const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); + const cfield = ComputedField.DisableCompute(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); } @@ -588,7 +587,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const dragData = de.complete.docDragData; if (dragData) { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const layoutProto = DocCast(this.layoutDoc.proto); + const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); const draggedDoc = dragData.droppedDocuments.lastElement(); let added: Opt<boolean>; @@ -596,7 +596,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) { // replace text contents when dragging with Alt if (de.altKey) { - const fieldKey = Doc.LayoutFieldKey(draggedDoc); + const fieldKey = Doc.LayoutDataKey(draggedDoc); if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } @@ -1003,16 +1003,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { link.$isDictation = true; - const audioanchor = Cast(link.link_anchor_2, Doc, null); - const textanchor = Cast(link.link_anchor_1, Doc, null); + const audioanchor = DocCast(link.link_anchor_2); + const textanchor = DocCast(link.link_anchor_1); if (audioanchor) { audioanchor.backgroundColor = 'tan'; const audiotag = this.EditorView.state.schema.nodes.audiotag.create({ timeCode: NumCast(audioanchor._timecodeToShow), audioId: audioanchor[Id], - textId: textanchor[Id], + textId: textanchor?.[Id] ?? '', }); - textanchor.$title = 'dictation:' + audiotag.attrs.timeCode; + textanchor && (textanchor.$title = 'dictation:' + audiotag.attrs.timeCode); const tr = this.EditorView.state.tr.insert(this.EditorView.state.doc.content.size, audiotag); const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); @@ -1147,11 +1147,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. // Since we also monitor all component height changes, this will update the document's height. - resetNativeHeight = (scrollHeight: number) => { - const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); + resetNativeHeight = action((scrollHeight: number) => { this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight; - if (nh) this.layoutDoc._nativeHeight = scrollHeight; - }; + if (!this.layoutDoc.isTemplateForField) this.layoutDoc._nativeHeight = scrollHeight; + }); addPlugin = (plugin: Plugin) => { const editorView = this.EditorView; @@ -1216,8 +1215,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const dataData = this.dataDoc[this.fieldKey]; const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey]; const dataTime = dataData ? (DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; - const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; - const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; @@ -1313,7 +1312,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // } catch (err) { // console.log('Drop failed', err); // } - this.addDocument?.(DocCast(this.Document.image)); + DocCast(this.Document.image) && this.addDocument?.(DocCast(this.Document.image)!); } //if (this.Document.image) this.addDocument?.(DocCast(this.Document.image)); @@ -1426,8 +1425,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { this.EditorView?.destroy(); + const edState = () => { + try { + return rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config); + } catch { + return EditorState.create(config); + } + }; this._editorView = new EditorView(this.ProseRef, { - state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), + state: edState(), handleScrollToSelection: this.scrollToSelection, dispatchTransaction: this.dispatchTransaction, nodeViews: FormattedTextBox._nodeViews(this), @@ -1439,7 +1445,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB (this.EditorView as unknown as { scrollToSelection: unknown }).scrollToSelection = this.scrollToSelection; const { state } = this._editorView; if (!rtfField) { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const layoutProto = DocCast(this.layoutDoc.proto); + const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const startupText = Field.toString(dataDoc[fieldKey] as FieldType); const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left'; if (textAlign !== 'left') { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 4fa1fb13b..d31a58030 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -148,11 +148,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._activeAlignment = this.getActiveAlignment(); this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox)); this._activeFontFamily = !activeFamilies.length - ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) + ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; @@ -382,7 +382,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } } else if (this.dataDoc) { - this.dataDoc[`${Doc.LayoutFieldKey(this.dataDoc)}_${fontField}`] = value; + this.dataDoc[`${Doc.LayoutDataKey(this.dataDoc)}_${fontField}`] = value; this.updateMenu(undefined, undefined, undefined, this.dataDoc); } else { Doc.UserDoc()[fontField] = value; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 14ea911b1..26ccf6931 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -108,25 +108,23 @@ export class RichTextRules { new InputRule(/>>$/, (state, match, start, end) => { const numInlines = NumCast(this.Document.$inlineTextCount); this.Document.$inlineTextCount = numInlines + 1; - const inlineFieldKey = '$inline' + numInlines; // which field on the text document this annotation will write to - const inlineLayoutKey = '$layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation + const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to + const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation const textDocInline = Docs.Create.TextDocument('', { - layout_fieldKey: inlineLayoutKey, _width: 75, _height: 35, - annotationOn: this.Document[DocData], _layout_fitWidth: true, _layout_autoHeight: true, - text_fontSize: '9px', - title: 'inline comment', }); + textDocInline.layout_fieldKey = inlineLayoutKey; + textDocInline.annotationOn = this.Document[DocData]; textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc - textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point + //textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.isDataDoc = true; textDocInline.proto = this.Document[DocData]; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] - this.Document[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text - this.Document[inlineFieldKey] = ''; // set a default value for the annotation + this.Document['$' + inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text + this.Document['$' + inlineFieldKey] = ''; // set a default value for the annotation const node = state.doc.resolve(start).nodeAfter; const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); @@ -287,7 +285,8 @@ export class RichTextRules { editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength)))); } - DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); + const tanchor = this.TextBox.getAnchor(true); + tanchor && DocUtils.MakeLink(tanchor, target, { link_relationship: 'portal to:portal from' }); const teditor = this.TextBox.EditorView; if (teditor && selection) { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 23155ebf3..11f35b8ef 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -149,7 +149,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { setIsLoading = action((input?: boolean) => { this._isLoading = !!input; }); // prettier-ignore setShowAIGalleryVisibilty = action((visible: boolean) => { this._showAIGallery = visible; }); // prettier-ignore setBezierControlPoints = action((newPoints: { p1: number[]; p2: number[] }) => { - this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); + this.activeItem && this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); }); @computed get showEaseFunctions() { @@ -158,7 +158,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get currCPoints() { - return EaseFuncToPoints(this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'); + return EaseFuncToPoints(this.activeItem?.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'); } @computed @@ -175,7 +175,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocListCast(this.Document[this.presFieldKey]); } @computed get tagDocs() { - return this.childDocs.map(doc => Cast(doc.presentation_targetDoc, Doc, null)); + return this.childDocs.map(doc => DocCast(doc.presentation_targetDoc)!).filter(doc => doc); } @computed get itemIndex() { return NumCast(this.Document._itemIndex); @@ -187,11 +187,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocCast(this.activeItem?.presentation_targetDoc); } public static targetRenderedDoc = (doc: Doc) => { - const targetDoc = Cast(doc?.presentation_targetDoc, Doc, null); + const targetDoc = DocCast(doc?.presentation_targetDoc); return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; }; @computed get scrollable() { - if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true; + if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc?.type as DocumentType) || this.targetDoc?._type_collection === CollectionViewType.Stacking) return true; return false; } @computed get selectedDocumentView() { @@ -273,8 +273,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; stopTempMedia = (targetDocField: FieldResult) => { - const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField); - if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as DocumentType)) { + const targetDoc = DocCast(DocCast(targetDocField)?.annotationOn) ?? DocCast(targetDocField); + if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc?.type as DocumentType)) { const targMedia = DocumentView.getDocumentView(targetDoc); targMedia?.ComponentView?.Pause?.(); } @@ -295,13 +295,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.setIsLoading(true); const slideDefaults: { [key: string]: FieldResult } = { presentation_transition: 500, config_zoom: 1 }; const currSlideProperties = gptSlideProperties.reduce( - (prev, key) => { prev[key] = Field.toString(this.activeItem[key]) ?? prev[key]; return prev; }, + (prev, key) => { prev[key] = Field.toString(this.activeItem?.[key]) ?? prev[key]; return prev; }, slideDefaults); // prettier-ignore gptTrailSlideCustomization(input, JSON.stringify(currSlideProperties)) .then(res => (Object.entries(JSON.parse(res)) as string[][]).forEach(([key, val]) => { - this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key]); + this.activeItem && (this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key])); }) ) .catch(e => console.error(e)) @@ -325,7 +325,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].presentation_groupWithUp) > 1; if (serial) { this.gotoDocument(nextSelected, this.activeItem, true, async () => { - const waitTime = NumCast(this.activeItem.presentation_duration); + const waitTime = NumCast(this.activeItem?.presentation_duration); await new Promise<void>(res => { setTimeout(res, Math.max(0, waitTime)); }); @@ -346,7 +346,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { progressivizedItems = (doc: Doc) => { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presentation_indexed !== undefined && targetList) { - const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); + const listItems = (Cast(targetList[Doc.LayoutDataKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutDataKey(targetList) + '_annotations']); return listItems.filter(ldoc => !ldoc.layout_unrendered); } return undefined; @@ -364,7 +364,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { next = () => { const progressiveReveal = (first: boolean) => { const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null); - if (presIndexed !== undefined) { + if (presIndexed !== undefined && this.activeItem) { const listItems = this.progressivizedItems(this.activeItem); const listItemDoc = listItems?.[presIndexed]; if (listItems && listItemDoc) { @@ -395,7 +395,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.childDocs[this.itemIndex + 1] !== undefined) { // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide const slides = DocListCast(this.Document[StrCast(this.presFieldKey, 'data')]); - const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex; + const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d) ?? d))) : this.itemIndex; // before moving onto next slide, run the subroutines :) const currentDoc = this.childDocs[this.itemIndex]; @@ -422,7 +422,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const { activeItem } = this; let prevSelected = this.itemIndex; // Functionality for group with up - let didZoom = activeItem.presentation_movement; + let didZoom = activeItem?.presentation_movement; for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].presentation_groupWithUp; prevSelected--) { didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presentation_movement : didZoom; } @@ -452,7 +452,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.stopTempMedia(from.presentation_targetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played - if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.presentation_mediaStart === 'auto') { + if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc?.type === DocumentType.AUDIO || this.targetDoc?.type === DocumentType.VID) && this.activeItem?.presentation_mediaStart === 'auto') { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); @@ -516,8 +516,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame; if (activeFrame !== undefined) { const frameTime = NumCast(activeItem.presentation_transition, 500); - const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc); - const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; + const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem?.presentation_targetDoc)?.embedContainer) : DocCast(activeItem.presentation_targetDoc); + const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext)?.annotationOn) : acontext; if (context) { const ffview = CollectionFreeFormView.from(DocumentView.getFirstDocumentView(context)); if (ffview?.childDocs) { @@ -528,7 +528,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if ((pinDataTypes?.dataview && activeItem.config_data !== undefined) || (!pinDataTypes && activeItem.config_data !== undefined)) { bestTarget._dataTransition = `all ${transTime}ms`; - const fkey = Doc.LayoutFieldKey(bestTarget); + const fkey = Doc.LayoutDataKey(bestTarget); const setData = bestTargetView?.ComponentView?.setData; if (setData) setData(activeItem.config_data); else { @@ -553,7 +553,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.config_clipWidth !== undefined)) { - const fkey = '_' + Doc.LayoutFieldKey(bestTarget); + const fkey = '_' + Doc.LayoutDataKey(bestTarget); if (bestTarget[fkey + '_clipWidth'] !== activeItem.config_clipWidth) { bestTarget[fkey + '_clipWidth'] = activeItem.config_clipWidth; changed = true; @@ -655,7 +655,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) { - const fkey = Doc.LayoutFieldKey(bestTarget); + const fkey = Doc.LayoutDataKey(bestTarget); const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered); const newItems = DocListCast(activeItem.config_annotations).map(doc => { doc.hidden = false; @@ -672,7 +672,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.config_pinLayoutData !== undefined)) { changed = true; - const layoutField = Doc.LayoutFieldKey(bestTarget); + const layoutField = Doc.LayoutDataKey(bestTarget); const transitioned = new Set<Doc>(); StrListCast(activeItem.config_pinLayoutData) .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string }) @@ -740,7 +740,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const { activeItem, targetDoc } = this; const finished = (options: FocusViewOptions) => { afterNav?.(options); - targetDoc[Animation] = undefined; + targetDoc && (targetDoc[Animation] = undefined); }; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); @@ -756,7 +756,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } finished(options); }); - PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); + targetDoc && activeItem && PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); }; static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) { @@ -806,42 +806,44 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action doHideBeforeAfter = () => { this.childDocs.forEach((doc, index) => { - const curDoc = Cast(doc, Doc, null); - const tagDoc = PresBox.targetRenderedDoc(curDoc); - const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc); - let opacity = index === this.itemIndex ? 1 : undefined; - if (curDoc.presentation_hide) { - if (index !== this.itemIndex) { - opacity = 1; + const curDoc = DocCast(doc); + if (curDoc) { + const tagDoc = PresBox.targetRenderedDoc(curDoc) ?? curDoc; + const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc); + let opacity = index === this.itemIndex ? 1 : undefined; + if (curDoc.presentation_hide) { + if (index !== this.itemIndex) { + opacity = 1; + } } - } - const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); - if (curDoc.presentation_hideBefore && index === hidingIndBef) { - if (index > this.itemIndex) { - opacity = 0; - } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { - opacity = 1; + const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); + if (curDoc.presentation_hideBefore && index === hidingIndBef) { + if (index > this.itemIndex) { + opacity = 0; + } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { + opacity = 1; + } } - } - const hidingIndAft = - itemIndexes - .slice() - .reverse() - .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); - if (curDoc.presentation_hideAfter && index === hidingIndAft) { - if (index < this.itemIndex) { - opacity = 0; - } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) { - opacity = 1; + const hidingIndAft = + itemIndexes + .slice() + .reverse() + .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); + if (curDoc.presentation_hideAfter && index === hidingIndAft) { + if (index < this.itemIndex) { + opacity = 0; + } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) { + opacity = 1; + } } - } - const hidingInd = itemIndexes.find(item => item === this.itemIndex); - if (curDoc.presentation_hide && index === hidingInd) { - if (index === this.itemIndex) { - opacity = 0; + const hidingInd = itemIndexes.find(item => item === this.itemIndex); + if (curDoc.presentation_hide && index === hidingInd) { + if (index === this.itemIndex) { + opacity = 0; + } } + opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); } - opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); }); }; @@ -940,7 +942,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { initializePresState = (startIndex: number) => { this.childDocs.forEach((doc, index) => { - const tagDoc = PresBox.targetRenderedDoc(doc); + const tagDoc = PresBox.targetRenderedDoc(doc) ?? doc; if (doc.presentation_hideBefore && index > startIndex) tagDoc.opacity = 0; if (doc.presentation_hideAfter && index < startIndex) tagDoc.opacity = 0; if (doc.presentation_indexed !== undefined && index >= startIndex) { @@ -969,7 +971,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc.presentation_status = PresStatus.Autoplay; this.initializePresState(startIndex); const func = () => { - const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition); + const delay = NumCast(this.activeItem?.presentation_duration, this.activeItem?.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem?.presentation_transition); this._presTimer = setTimeout(() => { if (this.next() === false) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; this.layoutDoc.presentation_status === PresStatus.Autoplay && func(); @@ -1060,7 +1062,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return !results.some(r => !r); }; - childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); + childLayoutTemplate = () => Docs.Create.PresSlideDocument(); removeDocument = (doc: Doc | Doc[]) => !toList(doc) .map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)) @@ -1077,13 +1079,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @computed get listOfSelected() { return Array.from(this.selectedArray).map((doc, index) => { - const curDoc = Cast(doc, Doc, null); + const curDoc = DocCast(doc); + if (!curDoc) return null; const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) return ( <div key={doc[Id]} className="selectedList-items"> <b> - {index + 1}. {StrCast(curDoc.title)}) + {index + 1}. {StrCast(curDoc.title)} </b> </div> ); @@ -1277,7 +1280,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presCollection = collection; const dv = DocumentView.getDocumentView(presCollection); this.childDocs.forEach((doc, index) => { - const tagDoc = PresBox.targetRenderedDoc(doc); + const tagDoc = PresBox.targetRenderedDoc(doc) ?? doc; const srcContext = Cast(tagDoc.embedContainer, Doc, null); const labelCreator = (top: number, left: number, edge: number, fontSize: number) => ( <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}> @@ -1526,17 +1529,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Applies the slide transiiton settings to all docs in the array @undoBatch applyTo = (array: Doc[]) => { - this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); - this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); - this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); - this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); - const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; - array.forEach(curDoc => { - curDoc.presentation_transition = pt; - curDoc.presentation_duration = pd; - curDoc.presentation_hideBefore = ph; - curDoc.presentation_hideAfter = pa; - }); + if (this.activeItem) { + this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); + this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); + this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); + this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); + const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; + array.forEach(curDoc => { + curDoc.presentation_transition = pt; + curDoc.presentation_duration = pd; + curDoc.presentation_hideBefore = ph; + curDoc.presentation_hideAfter = pa; + }); + } }; @computed get visibilityDurationDropdown() { @@ -1673,15 +1678,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={() => { activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; - const tagDoc = PresBox.targetRenderedDoc(this.activeItem); + const tagDoc = PresBox.targetRenderedDoc(activeItem) ?? activeItem; const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0; // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. - let dataField = Doc.LayoutFieldKey(tagDoc); + let dataField = Doc.LayoutDataKey(tagDoc); if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations'; - if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); + if (DocCast(activeItem.presentation_targetDoc)?.annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); }}> Enable @@ -1797,12 +1802,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={() => { this.updateEffect(elem.effect, false); this.updateEffectDirection(elem.direction); - this.updateEffectTiming(this.activeItem, { - type: SpringType.CUSTOM, - stiffness: elem.stiffness, - damping: elem.damping, - mass: elem.mass, - }); + this.activeItem && + this.updateEffectTiming(this.activeItem, { + type: SpringType.CUSTOM, + stiffness: elem.stiffness, + damping: elem.damping, + mass: elem.mass, + }); }}> <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite> <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} /> @@ -1947,8 +1953,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { formLabelPlacement="left" closeOnSelect items={easeItems} - selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'} - setSelectedVal={val => typeof val === 'string' && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)} + selectedVal={this.activeItem?.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'} + setSelectedVal={val => typeof val === 'string' && this.activeItem && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)} dropdownType={DropdownType.SELECT} type={Type.TERT} /> @@ -2145,9 +2151,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get mediaOptionsDropdown() { const { activeItem } = this; if (activeItem && this.targetDoc) { - const renderTarget = PresBox.targetRenderedDoc(this.activeItem); + const renderTarget = PresBox.targetRenderedDoc(this.activeItem ?? this.targetDoc) ?? this.targetDoc; const clipStart = NumCast(renderTarget.clipStart); - const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration'])); + const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutDataKey(renderTarget) + '_duration'])); const configClipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); return ( <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> @@ -2699,7 +2705,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && - (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); + (this.activeItem?.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; const inOverlay = Doc.IsInMyOverlay(this.Document); // Case 1: There are still other frames and should go through all frames before going to next slide @@ -2892,7 +2898,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && - (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); + (this.activeItem?.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; return this._props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div @@ -2984,16 +2990,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> ) : null} </div> - {/* { - // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack - <Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}> - <button className="add-slide-button" onPointerDown={this.startMarqueeCreateSlide}> - + NEW SLIDE - </button> - </Tooltip> - } */} </div> - {/* presbox chatbox */} {this._chatActive && <div className="presBox-chatbox" />} </div> ); diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresSlideBox.scss index 9ac2b5a94..9ac2b5a94 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresSlideBox.scss diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresSlideBox.tsx index 7e0375275..3dbb3da88 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresSlideBox.tsx @@ -7,7 +7,7 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUt import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -25,16 +25,16 @@ import { returnEmptyDocViewList } from '../../StyleProvider'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { PresBox } from './PresBox'; -import './PresElementBox.scss'; +import './PresSlideBox.scss'; import { PresMovement } from './PresEnums'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. */ @observer -export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { +export class PresSlideBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(PresElementBox, fieldKey); + return FieldView.LayoutString(PresSlideBox, fieldKey); } private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -56,7 +56,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { .containerViewPath?.() .slice() .reverse() - .find(dv => dv?.ComponentView instanceof PresBox)?.ComponentView as PresBox; + .find(dv => dv?.ComponentView instanceof PresBox)?.ComponentView as Opt<PresBox>; } // the presentation view document that renders this slide @@ -72,7 +72,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // this is the document in the workspaces that is targeted by the slide @computed get targetDoc() { - return Cast(this.slideDoc.presentation_targetDoc, Doc, null) || this.slideDoc; + return DocCast(this.slideDoc.presentation_targetDoc, this.slideDoc)!; } // computes index of this presentation slide in the presBox list @@ -118,7 +118,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return !this.slideDoc.presentation_expandInlineButton || !this.targetDoc ? null : ( <div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}> <DocumentView - Document={PresBox.targetRenderedDoc(this.slideDoc)} + Document={PresBox.targetRenderedDoc(this.slideDoc) ?? this.slideDoc} PanelWidth={this.embedWidth} PanelHeight={this.embedHeight} isContentActive={this._props.isContentActive} @@ -349,7 +349,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { /// remove all videos that have been recorded from overlay (leave videso that are being recorded to avoid losing data) static removeEveryExistingRecordingInOverlay = () => { - Doc.MyOverlayDocs.filter(doc => doc.slides !== null && PresElementBox.videoIsRecorded(DocCast(doc.slides))) // + Doc.MyOverlayDocs.filter(doc => doc.slides !== null && PresSlideBox.videoIsRecorded(DocCast(doc.slides))) // .forEach(Doc.RemFromMyOverlay); }; @@ -364,9 +364,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { showRecording = undoable( action((activeItem: Doc, iconClick: boolean = false) => { // remove the overlays on switch *IF* not opened from the specific icon - if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); + if (!iconClick) PresSlideBox.removeEveryExistingRecordingInOverlay(); - activeItem.recording && Doc.AddToMyOverlay(DocCast(activeItem.recording)); + DocCast(activeItem.recording) && Doc.AddToMyOverlay(DocCast(activeItem.recording)!); }), 'show video recording' ); @@ -374,7 +374,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { startRecording = undoable( action((e: React.MouseEvent, activeItem: Doc) => { e.stopPropagation(); - if (PresElementBox.videoIsRecorded(activeItem)) { + if (PresSlideBox.videoIsRecorded(activeItem)) { // if we already have an existing recording this.showRecording(activeItem, true); // // if we already have an existing recording @@ -383,7 +383,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // we dont have any recording // Remove every recording that already exists in overlay view // this is a design decision to clear to focus in on the recoding mode - PresElementBox.removeEveryExistingRecordingInOverlay(); + PresSlideBox.removeEveryExistingRecordingInOverlay(); // create and add a recording to the slide // make recording box appear in the bottom right corner of the screen @@ -413,7 +413,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { TreeView.ToggleChildrenRun.get(this.slideDoc)?.(); // call this.slideDoc.recurChildren() to get all the children - // if (iconClick) PresElementBox.showVideo = false; + // if (iconClick) PresSlideBox.showVideo = false; }; @computed @@ -452,7 +452,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> ); items.push( - <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> + <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresSlideBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> <FontAwesomeIcon icon={`video${this.videoRecordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> </div> @@ -622,7 +622,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } } -Docs.Prototypes.TemplateMap.set(DocumentType.PRESELEMENT, { - layout: { view: PresElementBox, dataField: 'data' }, - options: { acl: '', title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, +Docs.Prototypes.TemplateMap.set(DocumentType.PRESSLIDE, { + layout: { view: PresSlideBox, dataField: 'data' }, + options: { acl: '', title: 'presSlide', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true }, }); diff --git a/src/client/views/nodes/trails/index.ts b/src/client/views/nodes/trails/index.ts index 7b18974df..a5bc55221 100644 --- a/src/client/views/nodes/trails/index.ts +++ b/src/client/views/nodes/trails/index.ts @@ -1,3 +1,3 @@ export * from './PresBox'; -export * from './PresElementBox'; +export * from './PresSlideBox'; export * from './PresEnums'; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index e3ee51424..c45d8e052 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -210,7 +210,7 @@ export class GPTPopup extends ObservableReactComponent<object> { generateFireflyImage = (imgDesc: string) => { const selView = DocumentView.Selected().lastElement(); const selDoc = selView?.Document; - if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutFieldKey(selDoc)] instanceof ImageField)) { + if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutDataKey(selDoc)] instanceof ImageField)) { const oldPrompt = StrCast(selDoc.ai_firefly_prompt, StrCast(selDoc.title)); const newPrompt = oldPrompt ? `${oldPrompt} ~~~ ${imgDesc}` : imgDesc; return DrawingFillHandler.drawingToImage(selDoc, 100, newPrompt, selDoc) diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 86e28a768..f60eb486e 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -60,7 +60,7 @@ export class FaceRecognitionHandler { * @param imgDoc image with faces * @returns faceDoc array */ - public static ImageDocFaceAnnos = (imgDoc: Doc) => DocListCast(imgDoc[`${Doc.LayoutFieldKey(imgDoc)}_annotations`]).filter(doc => doc.face); + public static ImageDocFaceAnnos = (imgDoc: Doc) => DocListCast(imgDoc[`${Doc.LayoutDataKey(imgDoc)}_annotations`]).filter(doc => doc.face); /** * returns a list of all face collection Docs on the current dashboard @@ -207,7 +207,7 @@ export class FaceRecognitionHandler { } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) { setTimeout(() => this.classifyFacesInImage(imgDoc), 1000); } else { - const imgUrl = ImageCast(imgDoc[Doc.LayoutFieldKey(imgDoc)]); + const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]); if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined. Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 1f6e80bd1..ad0f56cb9 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -218,7 +218,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { .filter(d => d) // eslint-disable-next-line no-loop-func .map(async d => { - const fieldKey = Doc.LayoutFieldKey(d); + const fieldKey = Doc.LayoutDataKey(d); const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView'); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; const dataDocs = await DocListCastAsync(data); diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index 2260d1f73..0e234e966 100644 --- a/src/client/views/smartdraw/StickerPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -56,10 +56,10 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps if (!doc.savedAsSticker) { const docView = DocumentView.getDocumentView(doc); await docView?.ComponentView?.updateIcon?.(true); - const { clone } = await Doc.MakeClone(doc); + const { clone } = Doc.MakeClone(doc); clone.title = doc.title; - const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; - Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image)); + const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutDataKey(clone)]))?.url?.href; + Doc.MyStickers && Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image)); doc.savedAsSticker = true; } }; @@ -327,9 +327,10 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps ); renderPaletteView = () => ( <> - {this.renderDoc(Doc.MyStickers, (r: DocumentView) => { - this._docView = r; - })} + {Doc.MyStickers && + this.renderDoc(Doc.MyStickers, (r: DocumentView) => { + this._docView = r; + })} <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode(StickerPaletteMode.create)} /> </> ); diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index 8ac502348..37586fcc0 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -13,7 +13,7 @@ import { makeInterface } from '../fields/Schema'; class Repl extends React.Component { @observable text: string = ''; - @observable executedCommands: { command: string; result: any }[] = []; + @observable executedCommands: { command: string; result: unknown }[] = []; onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { this.text = e.target.value; @@ -30,7 +30,7 @@ class Repl extends React.Component { if (!script.compiled) { this.executedCommands.push({ command: this.text, result: 'Compile Error' }); } else { - const result = script.run({ makeInterface }, err => this.executedCommands.push({ command: this.text, result: err.message || err })); + const result = script.run({ makeInterface }, err => this.executedCommands.push({ command: this.text, result: err })); result.success && this.executedCommands.push({ command: this.text, result: result.result }); } this.text = ''; @@ -40,7 +40,7 @@ class Repl extends React.Component { @computed get commands() { return this.executedCommands.map(command => ( - <div style={{ marginTop: '5px' }}> + <div key={command.command} style={{ marginTop: '5px' }}> <p>{command.command}</p> {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */} <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}</pre> 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; |