diff options
Diffstat (limited to 'src/fields')
-rw-r--r-- | src/fields/Doc.ts | 189 | ||||
-rw-r--r-- | src/fields/DocSymbols.ts | 2 | ||||
-rw-r--r-- | src/fields/List.ts | 104 | ||||
-rw-r--r-- | src/fields/ObjectField.ts | 27 | ||||
-rw-r--r-- | src/fields/Proxy.ts | 31 | ||||
-rw-r--r-- | src/fields/RefField.ts | 2 | ||||
-rw-r--r-- | src/fields/RichTextUtils.ts | 60 | ||||
-rw-r--r-- | src/fields/ScriptField.ts | 28 | ||||
-rw-r--r-- | src/fields/util.ts | 148 |
9 files changed, 319 insertions, 272 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 2792f3aba..7abba7679 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable default-param-last */ /* eslint-disable no-use-before-define */ import { action, computed, makeObservable, observable, ObservableMap, ObservableSet, runInAction } from 'mobx'; import { computedFn } from 'mobx-utils'; @@ -19,7 +16,7 @@ import { import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { InkTool } from './InkField'; import { List } from './List'; -import { ObjectField } from './ObjectField'; +import { ObjectField, serverOpType } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { FieldId, RefField } from './RefField'; import { RichTextField } from './RichTextField'; @@ -28,6 +25,15 @@ import { ComputedField, ScriptField } from './ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types'; import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util'; +export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>; +export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>; + +export function SetObjGetRefField(func: (id: string, force?: boolean) => Promise<Doc | undefined>) { + ObjGetRefField = func; +} +export function SetObjGetRefFields(func: (ids: string[]) => Promise<Map<string, Doc | undefined>>) { + ObjGetRefFields = func; +} export const LinkedTo = '-linkedTo'; export namespace Field { /** @@ -91,18 +97,19 @@ export namespace Field { }); return script; } - export function toString(field: FieldType) { + export function toString(fieldIn: unknown) { + const field = fieldIn as FieldType; if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field); return field?.[ToString]?.() || ''; } - export function IsField(field: any): field is FieldType; - export function IsField(field: any, includeUndefined: true): field is FieldType | undefined; - export function IsField(field: any, includeUndefined: boolean = false): field is FieldType | undefined { + export function IsField(field: unknown): field is FieldType; + export function IsField(field: unknown, includeUndefined: true): field is FieldType | undefined; + export function IsField(field: unknown, includeUndefined: boolean = false): field is FieldType | undefined { return ['string', 'number', 'boolean'].includes(typeof field) || field instanceof ObjectField || field instanceof RefField || (includeUndefined && field === undefined); } // eslint-disable-next-line @typescript-eslint/no-shadow - export function Copy(field: any) { - return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; + export function Copy(field: unknown) { + return field instanceof ObjectField ? ObjectField.MakeCopy(field) : (field as FieldType); } UndoManager.SetFieldPrinter(toString); } @@ -118,9 +125,7 @@ export type FieldResult<T extends FieldType = FieldType> = Opt<T> | FieldWaiting * If no default value is given, and the returned value is not undefined, it can be safely modified. */ export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>; -// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>; -// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { const list = Cast(field, listSpec(Doc)); return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); @@ -157,7 +162,7 @@ export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol; im // this recursively updates all protos as well. export function updateCachedAcls(doc: Doc) { if (doc) { - const target = (doc as any)?.__fieldTuples ?? doc; + const target = doc[FieldTuples] ?? doc; const permissions: { [key: string]: symbol } = !target.author || target.author === ClientUtils.CurrentUserEmail() ? { acl_Me: AclAdmin } : {}; Object.keys(target).forEach(key => { key.startsWith('acl_') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl); @@ -177,12 +182,11 @@ export function updateCachedAcls(doc: Doc) { } @scriptingGlobal -@Deserializable('Doc', updateCachedAcls, ['id']) +@Deserializable('Doc', (obj: unknown) => updateCachedAcls(obj as Doc), ['id']) export class Doc extends RefField { @observable public static RecordingEvent = 0; @observable public static GuestDashboard: Doc | undefined = undefined; @observable public static GuestTarget: Doc | undefined = undefined; - @observable public static GuestMobile: Doc | undefined = undefined; @observable.shallow public static CurrentlyLoading: Doc[] = observable([]); // DocServer api public static FindDocByTitle(title: string) { @@ -219,6 +223,8 @@ export class Doc extends RefField { public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore public static get MyDockedBtns() { return DocCast(Doc.UserDoc().myDockedBtns); } // prettier-ignore public static get MySearcher() { return DocCast(Doc.UserDoc().mySearcher); } // prettier-ignore + public static get MyImageGrouper() { return DocCast(Doc.UserDoc().myImageGrouper); } //prettier-ignore + public static get MyFaceCollection() { return DocCast(Doc.UserDoc().myFaceCollection); } //prettier-ignore public static get MyHeaderBar() { return DocCast(Doc.UserDoc().myHeaderBar); } // prettier-ignore public static get MyLeftSidebarMenu() { return DocCast(Doc.UserDoc().myLeftSidebarMenu); } // prettier-ignore public static get MyLeftSidebarPanel() { return DocCast(Doc.UserDoc().myLeftSidebarPanel); } // prettier-ignore @@ -252,16 +258,16 @@ export class Doc extends RefField { public static set ActiveDashboard(val: Opt<Doc>) { Doc.UserDoc().activeDashboard = val; } // prettier-ignore public static IsInMyOverlay(doc: Doc) { return Doc.MyOverlayDocs.includes(doc); } // prettier-ignore - public static AddToMyOverlay(doc: Doc) { return Doc.ActiveDashboard?.myOverlayDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore - public static RemFromMyOverlay(doc: Doc) { return Doc.ActiveDashboard?.myOverlayDocs ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore + public static AddToMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore + public static RemFromMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore public static AddToMyPublished(doc: Doc) { doc[DocData].title_custom = true; doc[DocData].layout_showTitle = 'title'; - Doc.ActiveDashboard?.myPublishedDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore + Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore public static RemFromMyPublished(doc: Doc){ doc[DocData].title_custom = false; doc[DocData].layout_showTitle = undefined; - Doc.ActiveDashboard?.myPublishedDocs ? 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) { @@ -321,19 +327,22 @@ export class Doc extends RefField { }); this[SelfProxy] = docProxy; if (!id || forceSave) { - DocServer.CreateField(docProxy); + DocServer.CreateDocField(docProxy); } // eslint-disable-next-line no-constructor-return return docProxy; // need to return the proxy from the constructor so that all our added fields will get called } [key: string]: FieldResult; + [key2: symbol]: unknown; @serializable(alias('fields', map(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fieldTuples() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get __fieldTuples(): any { + // __fieldTuples does not follow the index signature pattern which requires a FieldResult return value -- so this hack suppresses the error return this[FieldTuples]; } - private set __fieldTuples(value) { + set __fieldTuples(value) { // called by deserializer to set all fields in one shot this[FieldTuples] = value; Object.keys(value).forEach(key => { @@ -348,33 +357,33 @@ export class Doc extends RefField { }); } - @observable private [FieldTuples]: any = {}; - @observable private [FieldKeys]: any = {}; + @observable private [FieldTuples]: { [key: string]: FieldResult } = {}; + @observable private [FieldKeys]: { [key: string]: boolean } = {}; /// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing @observable public [DocAcl]: { [key: string]: symbol } = {}; @observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout @observable public [DirectLinks] = new ObservableSet<Doc>(); - @observable public [AudioPlay]: any = undefined; // meant to store sound object from Howl + @observable public [AudioPlay]: unknown = undefined; // meant to store sound object from Howl @observable public [Animation]: Opt<Doc> = undefined; @observable public [Highlight]: boolean = false; @observable public [Brushed]: boolean = false; - @observable public [DocViews] = new ObservableSet<any /* DocumentView */>(); + @observable public [DocViews] = new ObservableSet<unknown /* DocumentView */>(); private [Self] = this; - private [SelfProxy]: any; + private [SelfProxy]: Doc; private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; - private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; + private [CachedUpdates]: { [key: string]: () => void | Promise<void> } = {}; public [Initializing]: boolean = false; - public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => { + public [FieldChanged] = (diff: { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: { start: number; deleteCount: number } } | undefined, serverOp: serverOpType) => { if (!this[UpdatingFromServer] || this[ForceServerWrite]) { DocServer.UpdateField(this[Id], serverOp); } }; public [Width] = () => NumCast(this[SelfProxy]._width); public [Height] = () => NumCast(this[SelfProxy]._height); - public [TransitionTimer]: any = undefined; + public [TransitionTimer]: NodeJS.Timeout | undefined = undefined; public [ToJavascriptString] = () => `idToDoc("${this[Self][Id]}")`; // what should go here? public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; @@ -387,7 +396,7 @@ export class Doc extends RefField { const self = this[SelfProxy]; const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null); if (templateLayoutDoc) { - let renderFieldKey: any; + let renderFieldKey: string = ''; const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; if (typeof layoutField === 'string') { [renderFieldKey] = layoutField.split("fieldKey={'")[1].split("'"); // layoutField.split("'")[1]; @@ -399,16 +408,17 @@ export class Doc extends RefField { return undefined; } - public async [HandleUpdate](diff: any) { - const set = diff.$set; + public async [HandleUpdate](diff: { $set: { [key: string]: FieldType } } | { $unset?: unknown }) { + const $set = '$set' in diff ? diff.$set : undefined; + const $unset = '$unset' in diff ? diff.$unset : undefined; const sameAuthor = this.author === ClientUtils.CurrentUserEmail(); const fprefix = 'fields.'; - Object.keys(set ?? {}) + Object.keys($set ?? {}) .filter(key => key.startsWith(fprefix)) .forEach(async key => { const fKey = key.substring(fprefix.length); const fn = async () => { - const value = await SerializationHelper.Deserialize(set[key]); + const value = (await SerializationHelper.Deserialize($set?.[key])) as FieldType; const prev = GetEffectiveAcl(this); this[UpdatingFromServer] = true; this[fKey] = value; @@ -423,14 +433,12 @@ export class Doc extends RefField { const writeMode = DocServer.getFieldWriteMode(fKey); if (fKey.startsWith('acl_') || writeMode !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; - // eslint-disable-next-line no-await-in-loop await fn(); } else { this[CachedUpdates][fKey] = fn; } }); - const unset = diff.$unset; - Object.keys(unset ?? {}) + Object.keys($unset ?? {}) .filter(key => key.startsWith(fprefix)) .forEach(async key => { const fKey = key.substring(7); @@ -451,12 +459,10 @@ export class Doc extends RefField { // eslint-disable-next-line no-redeclare export namespace Doc { - // eslint-disable-next-line import/no-mutable-exports export let SelectOnLoad: Doc | undefined; export function SetSelectOnLoad(doc: Doc | undefined) { SelectOnLoad = doc; } - // eslint-disable-next-line import/no-mutable-exports export let DocDragDataName: string = ''; export function SetDocDragDataName(name: string) { DocDragDataName = name; @@ -474,7 +480,7 @@ export namespace Doc { delete doc[CachedUpdates][field]; } } - export function AddCachedUpdate(doc: Doc, field: string, oldValue: any) { + export function AddCachedUpdate(doc: Doc, field: string, oldValue: FieldType) { const val = oldValue; doc[CachedUpdates][field] = () => { doc[UpdatingFromServer] = true; @@ -493,7 +499,7 @@ export namespace Doc { export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult { try { - return getField(doc[Self], key, ignoreProto); + return getField(doc[Self], key, ignoreProto) as FieldResult; } catch { return doc; } @@ -558,9 +564,12 @@ export namespace Doc { export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<FieldType>>>, skipUndefineds: boolean = false, isInitializing = false) { isInitializing && (doc[Initializing] = true); Object.keys(fields).forEach(key => { - const value = (fields as any)[key]; + const value = (fields as { [key: string]: Opt<FieldType> })[key]; if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds? + if (typeof value === 'object' && 'values' in value) { + console.log(value); + } doc[key] = value; } }); @@ -712,7 +721,7 @@ export namespace Doc { await Promise.all( Object.keys(doc).map(async key => { if (filter.includes(key)) return; - const assignKey = (val: any) => { + const assignKey = (val: Opt<FieldType>) => { copy[key] = val; }; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); @@ -735,7 +744,7 @@ export namespace Doc { .trim() ); const results = docids && (await DocServer.GetRefFields(docids)); - const rdocs = results && Array.from(Object.keys(results)).map(rkey => DocCast(results[rkey])); + 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 }); } @@ -782,9 +791,9 @@ export namespace Doc { linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } }); - if (Doc.Get(copy, 'title', true)) copy.title = '>:' + doc.title; - // Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); copy.cloneOf = doc; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc.title)); + if (Doc.Get(copy, 'title', true) && !(cfield instanceof ComputedField)) copy.title = '>:' + doc.title; cloneMap.set(doc[Id], copy); return copy; @@ -818,11 +827,11 @@ export namespace Doc { const linkedDocs = Array.from(linkMap.values()); linkedDocs.forEach(link => Doc.AddLink?.(link, true)); rtfMap.forEach(({ copy, key, field }) => { - const replacer = (match: any, attr: string, id: string /* , offset: any, string: any */) => { + 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: any, href: string, id: string /* , offset: any, string: any */) => { + const replacer2 = (match: string, href: string, id: string /* , offset: any, string: any */) => { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; @@ -874,7 +883,7 @@ export namespace Doc { newLayoutDoc.embedContainer = targetDoc; newLayoutDoc.resolvedDataDoc = dataDoc; newLayoutDoc.acl_Guest = SharingPermissions.Edit; - if (dataDoc[templateField] === undefined && (templateLayoutDoc[templateField] as any)?.length) { + if (dataDoc[templateField] === undefined && (templateLayoutDoc[templateField] as List<Doc>)?.length) { dataDoc[templateField] = ObjectField.MakeCopy(templateLayoutDoc[templateField] as List<Doc>); // ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"])`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); } @@ -901,7 +910,7 @@ export namespace Doc { return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc }; } - export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) { + 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)); @@ -976,9 +985,10 @@ export namespace Doc { } 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] = - doc[key] instanceof Doc && key.includes('layout[') - ? new ProxyField(Doc.MakeCopy(doc[key] as any)) // copy the expanded render template + docAtKey instanceof Doc && key.includes('layout[') + ? new ProxyField(Doc.MakeCopy(docAtKey)) // copy the expanded render template : ObjectField.MakeCopy(field); } else if (field instanceof Promise) { // eslint-disable-next-line no-debugger @@ -1235,7 +1245,7 @@ export namespace Doc { } const UnhighlightWatchers: (() => void)[] = []; - let UnhighlightTimer: any; + let UnhighlightTimer: NodeJS.Timeout | undefined; export function IsUnhighlightTimerSet() { return UnhighlightTimer; } // prettier-ignore export function AddUnHighlightWatcher(watcher: () => void) { if (UnhighlightTimer) { @@ -1244,7 +1254,7 @@ export namespace Doc { } export function linkFollowUnhighlight() { clearTimeout(UnhighlightTimer); - UnhighlightTimer = 0; + UnhighlightTimer = undefined; UnhighlightWatchers.forEach(watcher => watcher()); UnhighlightWatchers.length = 0; highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc)); @@ -1258,10 +1268,7 @@ export namespace Doc { if (UnhighlightTimer) clearTimeout(UnhighlightTimer); const presTransition = Number(presentationEffect?.presentation_transition); const duration = isNaN(presTransition) ? 5000 : presTransition; - UnhighlightTimer = window.setTimeout(() => { - linkFollowUnhighlight(); - UnhighlightTimer = 0; - }, duration); + UnhighlightTimer = setTimeout(linkFollowUnhighlight, duration); } export const highlightedDocs = new ObservableSet<Doc>(); @@ -1319,7 +1326,7 @@ export namespace Doc { StrCast(doc.layout_fieldKey).split('_')[1] === 'icon' && setNativeView(doc); } - export function setNativeView(doc: any) { + export function setNativeView(doc: Doc) { const prevLayout = StrCast(doc.layout_fieldKey).split('_')[1]; const deiconify = prevLayout === 'icon' && StrCast(doc.deiconifyLayout) ? 'layout_' + StrCast(doc.deiconifyLayout) : ''; prevLayout === 'icon' && (doc.deiconifyLayout = undefined); @@ -1356,15 +1363,15 @@ export namespace Doc { // filters document in a container collection: // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined - export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) { + export function setDocFilter(container: Opt<Doc>, key: string, value: FieldType | undefined, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) { if (!container) return; const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters'; const childFilters = StrListCast(container[filterField]); runInAction(() => { for (let i = 0; i < childFilters.length; i++) { const fields = childFilters[i].split(FilterSep); // split key:value:modifier - if (fields[0] === key && (fields[1] === value.toString() || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { - if (fields[2] === modifiers && modifiers && fields[1] === value.toString()) { + if (fields[0] === key && (fields[1] === value?.toString() || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { + if (fields[2] === modifiers && modifiers && fields[1] === value?.toString()) { // eslint-disable-next-line no-param-reassign if (toggle) modifiers = 'remove'; else return; @@ -1393,7 +1400,7 @@ export namespace Doc { return undefined; } export function assignDocToField(doc: Doc, field: string, id: string) { - DocServer.GetRefField(id).then(layout => { + DocServer.GetRefField(id)?.then(layout => { layout instanceof Doc && (doc[field] = layout); }); return id; @@ -1417,7 +1424,7 @@ export namespace Doc { export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) { DocServer.GetRefFields(docids).then(async fieldlist => { - const list = Array.from(Object.values(fieldlist)) + 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; @@ -1460,7 +1467,6 @@ export namespace Doc { case DocumentType.BUTTON: return 'bolt'; case DocumentType.PRES: return 'route'; case DocumentType.SCRIPTING: return 'terminal'; - case DocumentType.IMPORT: return 'cloud-upload-alt'; case DocumentType.VID: return 'video'; case DocumentType.INK: return 'pen-nib'; case DocumentType.PDF: return 'file-pdf'; @@ -1494,7 +1500,7 @@ export namespace Doc { const doc = DocCast(await DocServer.GetRefField(json.id)); const links = await DocServer.GetRefFields(json.linkids as string[]); Array.from(Object.keys(links)) - .map(key => links[key]) + .map(key => links.get(key)) .forEach(link => link instanceof Doc && Doc.AddLink?.(link)); return doc; } @@ -1506,7 +1512,7 @@ export namespace Doc { const primitives = ['string', 'number', 'boolean']; export interface JsonConversionOpts { - data: any; + data: unknown; title?: string; appendToExisting?: { targetDoc: Doc; fieldKey?: string }; excludeEmptyObjects?: boolean; @@ -1561,15 +1567,16 @@ export namespace Doc { if (data === undefined || data === null || ![...primitives, 'object'].includes(typeof data)) { return undefined; } - let resolved: any; + let resolved: unknown; try { resolved = JSON.parse(typeof data === 'string' ? data : JSON.stringify(data)); } catch (e) { + console.error(e); return undefined; } let output: Opt<Doc>; if (typeof resolved === 'object' && !(resolved instanceof Array)) { - output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc); + output = convertObject(resolved as { [key: string]: FieldType }, excludeEmptyObjects, title, appendToExisting?.targetDoc); } else { // give the proper types to the data extracted from the JSON const result = toField(resolved, excludeEmptyObjects); @@ -1590,7 +1597,7 @@ export namespace Doc { * @returns the object mapped from JSON to field values, where each mapping * might involve arbitrary recursion (since toField might itself call convertObject) */ - const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { + const convertObject = (object: { [key: string]: FieldType }, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { const hasEntries = Object.keys(object).length; if (hasEntries || !excludeEmptyObjects) { const resolved = target ?? new Doc(); @@ -1618,7 +1625,7 @@ export namespace Doc { * @returns the list mapped from JSON to field values, where each mapping * might involve arbitrary recursion (since toField might itself call convertList) */ - const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<FieldType>> => { + const convertList = (list: Array<unknown>, excludeEmptyObjects: boolean): Opt<List<FieldType>> => { const target = new List(); let result: Opt<FieldType>; // if excludeEmptyObjects is true, any qualifying conversions from toField will @@ -1633,29 +1640,33 @@ export namespace Doc { return undefined; }; - const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<FieldType> => { + const toField = (data: unknown, excludeEmptyObjects: boolean, title?: string): Opt<FieldType> => { if (data === null || data === undefined) { return undefined; } if (primitives.includes(typeof data)) { - return data; + return data as FieldType; } if (typeof data === 'object') { - return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined); + return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data as { [key: string]: FieldType }, excludeEmptyObjects, title, undefined); } throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); }; } } +export function returnEmptyDoclist() { + return [] as Doc[]; +} + export function RTFIsFragment(html: string) { return html.indexOf('data-pm-slice') !== -1; } export function GetHrefFromHTML(html: string): string { const parser = new DOMParser(); const parsedHtml = parser.parseFromString(html, 'text/html'); - if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) { - return (parsedHtml.body.childNodes[0].childNodes[0] as any).href; + if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as HTMLAnchorElement).href) { + return (parsedHtml.body.childNodes[0].childNodes[0] as HTMLAnchorElement).href; } return ''; } @@ -1675,35 +1686,35 @@ export function IdToDoc(id: string) { return DocCast(DocServer.GetCachedRefField(id)); } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function idToDoc(id: string): any { +ScriptingGlobals.add(function idToDoc(id: string): Doc { return IdToDoc(id); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function renameEmbedding(doc: any) { +ScriptingGlobals.add(function renameEmbedding(doc: Doc) { return StrCast(doc[DocData].title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getProto(doc: any) { +ScriptingGlobals.add(function getProto(doc: Doc) { return Doc.GetProto(doc); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getDocTemplate(doc?: any) { +ScriptingGlobals.add(function getDocTemplate(doc?: Doc) { return Doc.getDocTemplate(doc); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getEmbedding(doc: any) { +ScriptingGlobals.add(function getEmbedding(doc: Doc) { return Doc.MakeEmbedding(doc); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { +ScriptingGlobals.add(function getCopy(doc: Doc, copyProto: boolean) { return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function copyField(field: any) { +ScriptingGlobals.add(function copyField(field: FieldResult) { return Field.Copy(field); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function docList(field: any) { +ScriptingGlobals.add(function docList(field: FieldResult) { return DocListCast(field); }); // eslint-disable-next-line prefer-arrow-callback @@ -1711,11 +1722,11 @@ ScriptingGlobals.add(function addDocToList(doc: Doc, field: string, added: Doc) return Doc.AddDocToList(doc, field, added); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) { +ScriptingGlobals.add(function setInPlace(doc: Doc, field: string, value: string) { return Doc.SetInPlace(doc, field, value, false); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { +ScriptingGlobals.add(function sameDocs(doc1: Doc, doc2: Doc) { return Doc.AreProtosEqual(doc1, doc2); }); // eslint-disable-next-line prefer-arrow-callback @@ -1723,7 +1734,7 @@ ScriptingGlobals.add(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function docCastAsync(doc: FieldResult): any { +ScriptingGlobals.add(function docCastAsync(doc: FieldResult): FieldResult<Doc> { return Cast(doc, Doc); }); // eslint-disable-next-line prefer-arrow-callback @@ -1732,7 +1743,7 @@ ScriptingGlobals.add(function activePresentationItem() { return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: 'match' | 'check' | 'x' | 'remove') { +ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: string, modifiers: 'match' | 'check' | 'x' | 'remove') { Doc.setDocFilter(container, key, value, modifiers); }); // eslint-disable-next-line prefer-arrow-callback diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts index 837fcc90e..dc18d8638 100644 --- a/src/fields/DocSymbols.ts +++ b/src/fields/DocSymbols.ts @@ -1,3 +1,5 @@ +// NOTE: These symbols must be added to Doc.ts constructor !! + // Symbols for fundamental Doc operations such as: permissions, field and proxy access and server interactions export const AclPrivate = Symbol('DocAclOwnerOnly'); export const AclReadonly = Symbol('DocAclReadOnly'); diff --git a/src/fields/List.ts b/src/fields/List.ts index 38c47d546..22bbcb9ab 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -2,33 +2,33 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { alias, list as serializrList, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper'; -import { Field, FieldType, StrListCast } from './Doc'; +import { Doc, Field, FieldType, ObjGetRefFields, StrListCast } from './Doc'; import { FieldTuples, Self, SelfProxy } from './DocSymbols'; import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -import { ObjGetRefFields, ObjectField } from './ObjectField'; +import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; import { containedFieldChangedHandler, deleteProperty, getter, setter } from './util'; function toObjectField(field: FieldType) { - return field instanceof RefField ? new ProxyField(field) : field; + return field instanceof Doc ? new ProxyField(field) : field; } -function toRealField(field: FieldType) { +function toRealField(field: FieldType | undefined) { return field instanceof ProxyField ? field.value : field; } -type StoredType<T extends FieldType> = T extends RefField ? ProxyField<T> : T; +type StoredType<T extends FieldType> = T extends Doc ? ProxyField<T> : T; export const ListFieldName = 'fields'; @Deserializable('list') -class ListImpl<T extends FieldType> extends ObjectField { - static listHandlers: any = { +export class ListImpl<T extends FieldType> extends ObjectField { + static listHandlers = { /// Mutator methods - copyWithin() { + copyWithin: function (this: ListImpl<FieldType>) { throw new Error('copyWithin not supported yet'); }, - fill(value: any, start?: number, end?: number) { + fill: function (this: ListImpl<FieldType>, value: FieldType, start?: number, end?: number) { if (value instanceof RefField) { throw new Error('fill with RefFields not supported yet'); } @@ -36,12 +36,12 @@ class ListImpl<T extends FieldType> extends ObjectField { this[SelfProxy][FieldChanged]?.(); return res; }, - pop(): any { + pop: function (this: ListImpl<FieldType>): FieldType { const field = toRealField(this[Self].__fieldTuples.pop()); this[SelfProxy][FieldChanged]?.(); return field; }, - push: action(function (this: ListImpl<any>, ...itemsIn: any[]) { + push: action(function (this: ListImpl<FieldType>, ...itemsIn: FieldType[]) { const items = itemsIn.map(toObjectField); const list = this[Self]; @@ -58,27 +58,27 @@ class ListImpl<T extends FieldType> extends ObjectField { this[SelfProxy][FieldChanged]?.({ op: '$addToSet', items, length: length + items.length }); return res; }), - reverse() { + reverse: function (this: ListImpl<FieldType>) { const res = this[Self].__fieldTuples.reverse(); this[SelfProxy][FieldChanged]?.(); return res; }, - shift() { + shift: function (this: ListImpl<FieldType>) { const res = toRealField(this[Self].__fieldTuples.shift()); this[SelfProxy][FieldChanged]?.(); return res; }, - sort(cmpFunc: any) { + sort: function (this: ListImpl<FieldType>, cmpFunc: (first: FieldType | undefined, second: FieldType | undefined) => number) { this[Self].__realFields; // coerce retrieving entire array - const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); + const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: FieldType, second: FieldType) => cmpFunc(toRealField(first), toRealField(second)) : undefined); this[SelfProxy][FieldChanged]?.(); return res; }, - splice: action(function (this: any, start: number, deleteCount: number, ...itemsIn: any[]) { + splice: action(function (this: ListImpl<FieldType>, start: number, deleteCount: number, ...itemsIn: FieldType[]) { this[Self].__realFields; // coerce retrieving entire array const items = itemsIn.map(toObjectField); const list = this[Self]; - const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount); + const removed = list.__fieldTuples.filter((item: FieldType, i: number) => i >= start && i < start + deleteCount); for (let i = 0; i < items.length; i++) { const item = items[i]; // TODO Error checking to make sure parent doesn't already exist @@ -88,7 +88,7 @@ class ListImpl<T extends FieldType> extends ObjectField { item[FieldChanged] = containedFieldChangedHandler(this, i + start, item); } } - const hintArray: { val: any; index: number }[] = []; + const hintArray: { val: FieldType; index: number }[] = []; for (let i = start; i < start + deleteCount; i++) { hintArray.push({ val: list.__fieldTuples[i], index: i }); } @@ -104,7 +104,7 @@ class ListImpl<T extends FieldType> extends ObjectField { ); return res.map(toRealField); }), - unshift(...itemsIn: any[]) { + unshift: function (this: ListImpl<FieldType>, ...itemsIn: FieldType[]) { const items = itemsIn.map(toObjectField); const list = this[Self]; for (let i = 0; i < items.length; i++) { @@ -121,108 +121,108 @@ class ListImpl<T extends FieldType> extends ObjectField { return res; }, /// Accessor methods - concat: action(function (this: any, ...items: any[]) { + concat: action(function (this: ListImpl<FieldType>, ...items: FieldType[]) { this[Self].__realFields; return this[Self].__fieldTuples.map(toRealField).concat(...items); }), - includes(valueToFind: any, fromIndex: number) { + includes: function (this: ListImpl<FieldType>, valueToFind: FieldType, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields.includes(valueToFind, fromIndex); } return this[Self].__fieldTuples.includes(valueToFind, fromIndex); }, - indexOf(valueToFind: any, fromIndex: number) { + indexOf: function (this: ListImpl<FieldType>, valueToFind: FieldType, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields.indexOf(valueToFind, fromIndex); } return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); }, - join(separator: any) { + join: function (this: ListImpl<FieldType>, separator: string) { this[Self].__realFields; return this[Self].__fieldTuples.map(toRealField).join(separator); }, - lastElement() { + lastElement: function (this: ListImpl<FieldType>) { return this[Self].__realFields.lastElement(); }, - lastIndexOf(valueToFind: any, fromIndex: number) { + lastIndexOf: function (this: ListImpl<FieldType>, valueToFind: FieldType, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields.lastIndexOf(valueToFind, fromIndex); } return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); }, - slice(begin: number, end: number) { + slice: function (this: ListImpl<FieldType>, begin: number, end: number) { this[Self].__realFields; return this[Self].__fieldTuples.slice(begin, end).map(toRealField); }, /// Iteration methods - entries() { + entries: function (this: ListImpl<FieldType>) { return this[Self].__realFields.entries(); }, - every(callback: any, thisArg: any) { + every: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, array: FieldType[]) => unknown, thisArg: unknown) { return this[Self].__realFields.every(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - filter(callback: any, thisArg: any) { + filter: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, array: FieldType[]) => FieldType[], thisArg: unknown) { return this[Self].__realFields.filter(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - find(callback: any, thisArg: any) { + find: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, obj: FieldType[]) => FieldType, thisArg: unknown) { return this[Self].__realFields.find(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - findIndex(callback: any, thisArg: any) { + findIndex: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, obj: FieldType[]) => number, thisArg: unknown) { return this[Self].__realFields.findIndex(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - forEach(callback: any, thisArg: any) { + forEach: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, array: FieldType[]) => void, thisArg: unknown) { return this[Self].__realFields.forEach(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - map(callback: any, thisArg: any) { + map: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, array: FieldType[]) => unknown, thisArg: unknown) { return this[Self].__realFields.map(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - reduce(callback: any, initialValue: any) { + reduce: function (this: ListImpl<FieldType>, callback: (previousValue: unknown, currentValue: FieldType, currentIndex: number, array: FieldType[]) => unknown, initialValue: unknown) { return this[Self].__realFields.reduce(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, - reduceRight(callback: any, initialValue: any) { + reduceRight: function (this: ListImpl<FieldType>, callback: (previousValue: unknown, currentValue: FieldType, currentIndex: number, array: FieldType[]) => unknown, initialValue: unknown) { return this[Self].__realFields.reduceRight(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, - some(callback: any, thisArg: any) { + some: function (this: ListImpl<FieldType>, callback: (value: FieldType, index: number, array: FieldType[]) => boolean, thisArg: unknown) { return this[Self].__realFields.some(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, - values() { + values: function (this: ListImpl<FieldType>) { return this[Self].__realFields.values(); }, - [Symbol.iterator]() { + [Symbol.iterator]: function (this: ListImpl<FieldType>) { return this[Self].__realFields.values(); }, }; - static listGetter(target: any, prop: string | symbol, receiver: any): any { + static listGetter(target: ListImpl<FieldType>, prop: string | symbol, receiver: ListImpl<FieldType>): unknown { if (Object.prototype.hasOwnProperty.call(ListImpl.listHandlers, prop)) { - return ListImpl.listHandlers[prop]; + return (ListImpl.listHandlers as { [key: string | symbol]: unknown })[prop]; } return getter(target, prop, receiver); } @@ -251,7 +251,7 @@ class ListImpl<T extends FieldType> extends ObjectField { }, }); // eslint-disable-next-line no-use-before-define - this[SelfProxy] = list as any as List<FieldType>; // bcz: ugh .. don't know how to convince typesecript that list is a List + this[SelfProxy] = list as unknown as List<FieldType>; // bcz: ugh .. don't know how to convince typesecript that list is a List if (fields) { this[SelfProxy].push(...fields); } @@ -260,18 +260,20 @@ class ListImpl<T extends FieldType> extends ObjectField { } [key: number]: T | (T extends RefField ? Promise<T> : never); + [key2: symbol]: unknown; + [key3: string]: unknown; // this requests all ProxyFields at the same time to avoid the overhead // of separate network requests and separate updates to the React dom. @computed private get __realFields() { - const unrequested = this[FieldTuples].filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>); + const unrequested = this[FieldTuples].filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<Doc>); // if we find any ProxyFields that don't have a current value, then // start the server request for all of them if (unrequested.length) { const batchPromise = ObjGetRefFields(unrequested.map(p => p.fieldId)); // as soon as we get the fields from the server, set all the list values in one // action to generate one React dom update. - const allSetPromise = batchPromise.then(action(pfields => unrequested.map(toReq => toReq.setValue(pfields[toReq.fieldId])))); + const allSetPromise = batchPromise.then(action(pfields => unrequested.map(toReq => toReq.setValue(pfields.get(toReq.fieldId))))); // we also have to mark all lists items with this promise so that any calls to them // will await the batch request and return the requested field value. unrequested.forEach(p => p.setExternalValuePromise(allSetPromise)); @@ -280,11 +282,11 @@ class ListImpl<T extends FieldType> extends ObjectField { } @serializable(alias(ListFieldName, serializrList(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fieldTuples() { + get __fieldTuples() { return this[FieldTuples]; } - private set __fieldTuples(value) { + set __fieldTuples(value) { this[FieldTuples] = value; Object.keys(value).forEach(key => { const item = value[Number(key)]; @@ -297,7 +299,7 @@ class ListImpl<T extends FieldType> extends ObjectField { [Copy]() { const copiedData = this[Self].__fieldTuples.map(f => (f instanceof ObjectField ? f[Copy]() : f)); - const deepCopy = new ListImpl<T>(copiedData as any); + const deepCopy = new ListImpl<T>(copiedData as T[]); return deepCopy; } @@ -309,19 +311,19 @@ class ListImpl<T extends FieldType> extends ObjectField { private [SelfProxy]: List<FieldType>; // also used in utils.ts even though it won't be found using find all references [ToScriptString]() { return `new List(${this[ToJavascriptString]()})`; } // prettier-ignore - [ToJavascriptString]() { return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; } // prettier-ignore - [ToString]() { return `[${(this as any).map((field: any) => Field.toString(field))}]`; } // prettier-ignore + [ToJavascriptString]() { return `[${(this[FieldTuples]).map(field => Field.toScriptString(field))}]`; } // prettier-ignore + [ToString]() { return `[${(this[FieldTuples]).map(field => Field.toString(field))}]`; } // prettier-ignore } // 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>() +// 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 -export const List: { new <T extends FieldType>(fields?: T[]): List<T> } = ListImpl as any; +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); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function compareLists(l1: any, l2: any) { +ScriptingGlobals.add(function compareLists(l1: List<FieldType>, l2: List<FieldType>) { const L1 = StrListCast(l1); const L2 = StrListCast(l2); return !L1 && !L2 ? true : L1 && L2 && L1.length === L2.length && L2.reduce((p, v) => p && L1.includes(v), true); diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index 231086262..c533cb596 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -2,13 +2,27 @@ import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { RefField } from './RefField'; +export type serializedFieldType = { fieldId: string; heading?: string; __type: string }; +export type serializedFieldsType = { [key: string]: { fields: serializedFieldType[] } }; +export interface serializedDoctype { + readonly id: string; + readonly fields?: serializedFieldsType; +} + +export type serverOpType = { + $set?: serializedFieldsType; // + $unset?: { [key: string]: unknown }; + $remFromSet?: { [key: string]: { fields: serializedFieldType[] } | { deleteCount: number; start: number } | number | undefined; length: number; hint: { deleteCount: number; start: number } | undefined }; + $addToSet?: { [key: string]: { fields: serializedFieldType[] } | number | undefined; length: number }; +}; export abstract class ObjectField { // prettier-ignore public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; // eslint-disable-next-line no-use-before-define items: FieldType[] | undefined; length: number | undefined; - hint?: any }, serverOp?: any) => void; + hint?: { deleteCount: number, start: number} }, + serverOp?: serverOpType) => void; // eslint-disable-next-line no-use-before-define public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; @@ -22,15 +36,4 @@ export abstract class ObjectField { } export type FieldType = number | string | boolean | ObjectField | RefField; // bcz: hack for now .. must match the type definition in Doc.ts .. put here to avoid import cycles -// eslint-disable-next-line import/no-mutable-exports -export let ObjGetRefField: (id: string, force?: boolean) => Promise<RefField | undefined>; -// eslint-disable-next-line import/no-mutable-exports -export let ObjGetRefFields: (ids: string[]) => Promise<{ [id: string]: RefField | undefined }>; - -export function SetObjGetRefField(func: (id: string, force?: boolean) => Promise<RefField | undefined>) { - ObjGetRefField = func; -} -export function SetObjGetRefFields(func: (ids: string[]) => Promise<{ [id: string]: RefField | undefined }>) { - ObjGetRefFields = func; -} ScriptingGlobals.add(ObjectField); diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 83b5672b3..48c336e60 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -3,18 +3,19 @@ import { primitive, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Field, FieldWaiting, Opt } from './Doc'; +import { Doc, Field, FieldWaiting, Opt } from './Doc'; import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { ObjectField } from './ObjectField'; -import { RefField } from './RefField'; -function deserializeProxy(field: any) { +type serializedProxyType = { cache: { field: unknown; p: undefined | Promise<unknown> }; fieldId: string }; + +function deserializeProxy(field: serializedProxyType) { if (!field.cache.field) { - field.cache = { field: DocServer.GetCachedRefField(field.fieldId) as any, p: undefined }; + field.cache = { field: DocServer.GetCachedRefField(field.fieldId), p: undefined }; } } -@Deserializable('proxy', deserializeProxy) -export class ProxyField<T extends RefField> extends ObjectField { +@Deserializable('proxy', (obj: unknown) => deserializeProxy(obj as serializedProxyType)) +export class ProxyField<T extends Doc> extends ObjectField { constructor(); constructor(value: T); constructor(fieldId: string); @@ -39,10 +40,10 @@ export class ProxyField<T extends RefField> extends ObjectField { } [ToJavascriptString]() { - return Field.toScriptString(this[ToValue]()?.value); + return Field.toScriptString(this[ToValue]()?.value as T); } [ToScriptString]() { - return Field.toScriptString(this[ToValue]()?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? + 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); @@ -83,7 +84,7 @@ export class ProxyField<T extends RefField> extends ObjectField { return !!(!this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId)); } - setExternalValuePromise(externalValuePromise: Promise<any>) { + setExternalValuePromise(externalValuePromise: Promise<unknown>) { this.cache.p = externalValuePromise.then(() => this.value) as FieldWaiting<T>; } @action @@ -94,7 +95,7 @@ export class ProxyField<T extends RefField> extends ObjectField { } } -// eslint-disable-next-line no-redeclare +// eslint-disable-next-line no-redeclare, @typescript-eslint/no-namespace export namespace ProxyField { let useProxy = true; export function DisableProxyFields() { @@ -114,7 +115,7 @@ export namespace ProxyField { } } - export function toValue(value: any) { + export function toValue(value: { value: unknown }) { if (useProxy) { return { value: value.value }; } @@ -123,10 +124,10 @@ export namespace ProxyField { } // eslint-disable-next-line no-use-before-define -function prefetchValue(proxy: PrefetchProxy<RefField>) { - return proxy.value as any; +function prefetchValue(proxy: PrefetchProxy<Doc>) { + return proxy.value as Promise<Doc>; } @scriptingGlobal -@Deserializable('prefetch_proxy', prefetchValue) -export class PrefetchProxy<T extends RefField> extends ProxyField<T> {} +@Deserializable('prefetch_proxy', (obj:unknown) => prefetchValue(obj as PrefetchProxy<Doc>)) +export class PrefetchProxy<T extends Doc> extends ProxyField<T> {} diff --git a/src/fields/RefField.ts b/src/fields/RefField.ts index 1ce81368a..4ef2a6748 100644 --- a/src/fields/RefField.ts +++ b/src/fields/RefField.ts @@ -14,7 +14,7 @@ export abstract class RefField { this[Id] = this.__id; } - protected [HandleUpdate]?(diff: any): void | Promise<void>; + protected [HandleUpdate]?(diff: unknown): void | Promise<void>; abstract [ToJavascriptString](): string; abstract [ToScriptString](): string; diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 3763dcd2c..b3534dde7 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -1,9 +1,10 @@ +/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-use-before-define */ import { AssertionError } from 'assert'; import * as Color from 'color'; import { docs_v1 as docsV1 } from 'googleapis'; -import { Fragment, Mark, Node } from 'prosemirror-model'; +import { Fragment, Mark, Node, Schema } from 'prosemirror-model'; import { sinkListItem } from 'prosemirror-schema-list'; import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; import { ClientUtils, DashColor } from '../ClientUtils'; @@ -26,7 +27,7 @@ export namespace RichTextUtils { const joiner = ''; export const Initialize = (initial?: string) => { - const content: any[] = []; + const content: object[] = []; const state = { doc: { type: 'doc', @@ -80,8 +81,10 @@ export namespace RichTextUtils { // Preserve the current state, but re-write the content to be the blocks const parsed = JSON.parse(oldState ? oldState.Data : Initialize()); parsed.doc.content = elements.map(text => { - const paragraph: any = { type: 'paragraph' }; - text.length && (paragraph.content = [{ type: 'text', marks: [], text }]); // An empty paragraph gets treated as a line break + const paragraph: object = { + type: 'paragraph', + content: text.length ? [{ type: 'text', marks: [], text }] : undefined, // An empty paragraph gets treated as a line break + }; return paragraph; }); @@ -164,7 +167,7 @@ export namespace RichTextUtils { const inlineObjectMap = await parseInlineObjects(document); const title = document.title!; const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document); - let state = EditorState.create(new FormattedTextBox({} as any).config); + let state = EditorState.create(FormattedTextBox.MakeConfig()); const structured = parseLists(paragraphs); let position = 3; @@ -253,17 +256,20 @@ export namespace RichTextUtils { return groups; }; - const listItem = (lschema: any, runs: docsV1.Schema$TextRun[]): Node => lschema.node('list_item', null, paragraphNode(lschema, runs)); + const listItem = (lschema: Schema, runs: docsV1.Schema$TextRun[]): Node => lschema.node('list_item', null, paragraphNode(lschema, runs)); - const list = (lschema: any, items: Node[]): Node => lschema.node('ordered_list', { mapStyle: 'bullet' }, items); + const list = (lschema: Schema, items: Node[]): Node => lschema.node('ordered_list', { mapStyle: 'bullet' }, items); - const paragraphNode = (lschema: any, runs: docsV1.Schema$TextRun[]): Node => { - const children = runs.map(run => textNode(lschema, run)).filter(child => child !== undefined); + const paragraphNode = (lschema: Schema, runs: docsV1.Schema$TextRun[]): Node => { + const children = runs + .map(run => textNode(lschema, run)) + .filter(child => child !== undefined) + .map(child => child!); const fragment = children.length ? Fragment.from(children) : undefined; return lschema.node('paragraph', null, fragment); }; - const imageNode = (lschema: any, image: ImageTemplate, textNote: Doc) => { + const imageNode = (lschema: Schema, image: ImageTemplate, textNote: Doc) => { const { url: src, width, agnostic } = image; let docId: string; const guid = Utils.GenerateDeterministicGuid(agnostic); @@ -279,7 +285,7 @@ export namespace RichTextUtils { return lschema.node('image', { src, agnostic, width, docId, float: null }); }; - const textNode = (lschema: any, run: docsV1.Schema$TextRun) => { + const textNode = (lschema: Schema, run: docsV1.Schema$TextRun) => { const text = run.content!.removeTrailingNewlines(); return text.length ? lschema.text(text, styleToMarks(lschema, run.textStyle)) : undefined; }; @@ -291,29 +297,33 @@ export namespace RichTextUtils { ['fontSize', 'pFontSize'], ]); - const styleToMarks = (lschema: any, textStyle?: docsV1.Schema$TextStyle) => { + const styleToMarks = (lschema: Schema, textStyle?: docsV1.Schema$TextStyle) => { if (!textStyle) { return undefined; } const marks: Mark[] = []; Object.keys(textStyle).forEach(key => { const targeted = key as keyof docsV1.Schema$TextStyle; - const value = textStyle[targeted] as any; + const value = textStyle[targeted]; if (value) { - const attributes: any = {}; + const attributes: { [key: string]: number | string } = {}; let converted = StyleToMark.get(targeted) || targeted; - value.url && (attributes.href = value.url); - if (value.color) { - const object = value.color.rgbColor; - attributes.color = Color.rgb(['red', 'green', 'blue'].map(color => object[color] * 255 || 0)).hex(); + const urlValue = value as docsV1.Schema$Link; + urlValue.url && (attributes.href = urlValue.url); + const colValue = value as docsV1.Schema$OptionalColor; + const object = colValue.color?.rgbColor; + if (object) { + attributes.color = Color.rgb(['red', 'green', 'blue'].map(color => (object as { [key: string]: number })[color] * 255 || 0)).hex(); } - if (value.magnitude) { - attributes.fontSize = value.magnitude; + const magValue = value as docsV1.Schema$Dimension; + if (magValue.magnitude) { + attributes.fontSize = magValue.magnitude; } + const fontValue = value as docsV1.Schema$WeightedFontFamily; if (converted === 'weightedFontFamily') { - converted = ImportFontFamilyMapping.get(value.fontFamily) || 'timesNewRoman'; + converted = (fontValue.fontFamily && ImportFontFamilyMapping.get(fontValue.fontFamily)) || 'timesNewRoman'; } const mapped = lschema.marks[converted]; @@ -384,13 +394,11 @@ export namespace RichTextUtils { for (const markName of Object.keys(schema.marks)) { // eslint-disable-next-line no-cond-assign if (ignored.includes(markName) || !(mark = markMap[markName])) { - // eslint-disable-next-line no-continue continue; } let converted = MarkToStyle.get(markName) || (markName as keyof docsV1.Schema$TextStyle); - let value: any = true; + let value: unknown = true; if (!converted) { - // eslint-disable-next-line no-continue continue; } // eslint-disable-next-line @typescript-eslint/no-shadow @@ -402,10 +410,8 @@ export namespace RichTextUtils { const docDelimeter = '/doc/'; const alreadyShared = '?sharing=true'; if (new RegExp(window.location.origin + docDelimeter).test(url) && !url.endsWith(alreadyShared)) { - // eslint-disable-next-line no-await-in-loop const linkDoc = await DocServer.GetRefField(url.split(docDelimeter)[1]); if (linkDoc instanceof Doc) { - // eslint-disable-next-line no-await-in-loop let exported = (await Cast(linkDoc.link_anchor_2, Doc))!; if (!exported.customLayout) { exported = Doc.MakeEmbedding(exported); @@ -436,7 +442,7 @@ export namespace RichTextUtils { converted = 'fontSize'; value = { magnitude: parseInt(matches[1].replace('px', '')), unit: 'PT' }; } - textStyle[converted] = value; + textStyle[converted] = value as undefined; } if (Object.keys(textStyle).length) { requests.push(EncodeStyleUpdate(information)); diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 8fe365ac2..582c09f29 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,15 +1,15 @@ import { action, makeObservable, observable } from 'mobx'; import { computedFn } from 'mobx-utils'; import { PropSchema, SKIP, createSimpleSchema, custom, map, object, primitive, serializable } from 'serializr'; -import { numberRange } from '../Utils'; +import { emptyFunction, numberRange } from '../Utils'; import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT'; import { CompileScript, CompiledScript, ScriptOptions, Transformer } from '../client/util/Scripting'; import { ScriptingGlobals, scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable, autoObject } from '../client/util/SerializationHelper'; -import { Doc, Field, FieldType, FieldResult, Opt } from './Doc'; +import { Doc, Field, FieldType, FieldResult, ObjGetRefField, Opt } from './Doc'; import { Copy, FieldChanged, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; -import { ObjGetRefField, ObjectField } from './ObjectField'; +import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; function optional(propSchema: PropSchema) { @@ -20,7 +20,7 @@ function optional(propSchema: PropSchema) { } return SKIP; }, - (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { + (jsonValue, context, oldValue, callback) => { if (jsonValue !== undefined) { return propSchema.deserializer(jsonValue, callback, context, oldValue); } @@ -63,7 +63,7 @@ function finalizeScript(scriptIn: ScriptField) { async function deserializeScript(scriptIn: ScriptField) { const script = scriptIn; if (script.captures) { - const captured: any = {}; + const captured: { [key: string]: undefined | string | number | boolean | Doc } = {}; (script.script.options as ScriptOptions).capturedVariables = captured; Promise.all( script.captures.map(async capture => { @@ -85,7 +85,7 @@ async function deserializeScript(scriptIn: ScriptField) { } @scriptingGlobal -@Deserializable('script', deserializeScript) +@Deserializable('script', (obj: unknown) => deserializeScript(obj as ScriptField)) export class ScriptField extends ObjectField { @serializable readonly rawscript: string | undefined; @@ -114,7 +114,7 @@ export class ScriptField extends ObjectField { const captured = script?.options?.capturedVariables; if (captured) { - this.captures = new List<string>(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key].toString()))); + this.captures = new List<string>(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key]?.toString()))); } this.rawscript = rawscript; this.setterscript = setterscript; @@ -186,7 +186,7 @@ export class ScriptField extends ObjectField { } @scriptingGlobal -@Deserializable('computed', deserializeScript) +@Deserializable('computed', (obj: unknown) => deserializeScript(obj as ComputedField)) export class ComputedField extends ScriptField { static undefined = '__undefined'; static useComputed = true; @@ -221,7 +221,7 @@ export class ComputedField extends ScriptField { _readOnly_: true, }, console.log - ).result + ).result as FieldResult )(); // prettier-ignore return this._lastComputedResult; }; @@ -239,7 +239,7 @@ 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(() => undefined) as any as number[]); + const flist = new List<number>(numberRange(curTimecode + 1).map(emptyFunction) as unknown as number[]); flist[curTimecode] = Cast(doc[fieldKey], 'number', null); doc[`${fieldKey}_indexed`] = flist; } @@ -249,7 +249,7 @@ export class ComputedField extends ScriptField { } public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (!doc[`${fieldKey}_`]) { - const flist = new List<string>(numberRange(curTimecode + 1).map(() => undefined) as any as string[]); + const flist = new List<string>(numberRange(curTimecode + 1).map(emptyFunction) as unknown as string[]); flist[curTimecode] = StrCast(doc[fieldKey]); doc[`${fieldKey}_indexed`] = flist; } @@ -260,7 +260,7 @@ export class ComputedField extends ScriptField { public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (doc[`${fieldKey}`] instanceof List) return undefined; if (!doc[`${fieldKey}_indexed`]) { - const flist = new List<FieldType>(numberRange(curTimecode + 1).map(() => undefined) as any as FieldType[]); + const flist = new List<FieldType>(numberRange(curTimecode + 1).map(emptyFunction) as unknown as FieldType[]); flist[curTimecode] = Field.Copy(doc[fieldKey]); doc[`${fieldKey}_indexed`] = flist; } @@ -278,7 +278,7 @@ export class ComputedField extends ScriptField { ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback - function setIndexVal(list: any[], index: number, value: any) { + function setIndexVal(list: FieldResult[], index: number, value: FieldType) { while (list.length <= index) list.push(undefined); list[index] = value; }, @@ -288,7 +288,7 @@ ScriptingGlobals.add( ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback - function getIndexVal(list: any[], index: number, defaultVal: Opt<number> = undefined) { + function getIndexVal(list: unknown[], index: number, defaultVal: Opt<number> = undefined) { return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal); }, 'returns the value at a given index of a list', diff --git a/src/fields/util.ts b/src/fields/util.ts index a6499c3e3..60eadcdfd 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -7,8 +7,8 @@ import { UndoManager } from '../client/util/UndoManager'; import { Doc, DocListCast, FieldType, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, DirectLinks, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols'; -import { List } from './List'; -import { ObjectField } from './ObjectField'; +import { List, ListImpl } from './List'; +import { ObjectField, serializedFieldType, serverOpType } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { RefField } from './RefField'; import { RichTextField } from './RichTextField'; @@ -44,15 +44,23 @@ export function TraceMobx() { tracing && trace(); } -export const _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>(); +export const _propSetterCB = new Map<string, ((target: Doc, value: FieldType) => void) | undefined>(); -const _setterImpl = action((target: any, prop: string | symbol | number, valueIn: any, receiver: any): boolean => { +const _setterImpl = action((target: Doc | ListImpl<FieldType>, prop: string | symbol | number, valueIn: unknown, receiver: Doc | ListImpl<FieldType>): boolean => { + if (target instanceof ListImpl) { + if (typeof prop !== 'symbol' && +prop == prop) { + target[SelfProxy].splice(+prop, 1, valueIn as FieldType); + } else { + target[prop] = valueIn as FieldType; + } + return true; + } if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') { - target[prop] = valueIn; + target[prop] = valueIn as FieldResult; return true; } - let value = valueIn?.[SelfProxy] ?? valueIn; // convert any Doc type values to Proxy's + let value = (valueIn as Doc | ListImpl<FieldType>)?.[SelfProxy] ?? valueIn; // convert any Doc type values to Proxy's const curValue = target.__fieldTuples[prop]; if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { @@ -60,7 +68,7 @@ const _setterImpl = action((target: any, prop: string | symbol | number, valueIn // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way return true; } - if (value instanceof RefField) { + if (value instanceof Doc) { value = new ProxyField(value); } @@ -77,7 +85,7 @@ const _setterImpl = action((target: any, prop: string | symbol | number, valueIn delete curValue[FieldChanged]; } - if (typeof prop === 'string' && _propSetterCB.has(prop)) _propSetterCB.get(prop)!(target[SelfProxy], value); + if (typeof prop === 'string' && _propSetterCB.has(prop)) _propSetterCB.get(prop)!(target[SelfProxy], value as FieldType); // eslint-disable-next-line no-use-before-define const effectiveAcl = GetEffectiveAcl(target); @@ -104,20 +112,21 @@ const _setterImpl = action((target: any, prop: string | symbol | number, valueIn if (writeToServer) { // prettier-ignore - if (value === undefined) + if (value === undefined || value === null) (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } }); - else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}}); + else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: (value instanceof ObjectField ? SerializationHelper.Serialize(value) :value) as { fields: serializedFieldType[]}}}); if (prop === 'author' || prop.toString().startsWith('acl_')) updateCachedAcls(target); - } else { + } else if (receiver instanceof Doc) { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } !receiver[Initializing] && + receiver instanceof Doc && !StrListCast(receiver.undoIgnoreFields).includes(prop.toString()) && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent( { redo: () => { - receiver[prop] = value; + receiver[prop] = value as FieldType; }, undo: () => { const wasUpdate = receiver[UpdatingFromServer]; @@ -137,7 +146,7 @@ const _setterImpl = action((target: any, prop: string | symbol | number, valueIn return true; }); -let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl; +let _setter: (target: Doc | ListImpl<FieldType>, prop: string | symbol | number, value: FieldType | undefined, receiver: Doc | ListImpl<FieldType>) => boolean = _setterImpl; export function makeReadOnly() { _setter = _readOnlySetter; @@ -156,18 +165,18 @@ export function denormalizeEmail(email: string) { // return acl from cache or cache the acl and return. // eslint-disable-next-line no-use-before-define -const getEffectiveAclCache = computedFn((target: any, user?: string) => getEffectiveAcl(target, user), true); +const getEffectiveAclCache = computedFn((target: Doc | ListImpl<FieldType>, user?: string) => getEffectiveAcl(target, user), true); /** * Calculates the effective access right to a document for the current user. */ -export function GetEffectiveAcl(target: any, user?: string): symbol { +export function GetEffectiveAcl(target: Doc | ListImpl<FieldType>, user?: string): symbol { if (!target) return AclPrivate; if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail() === 'guest') return AclAdmin; return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } -export function GetPropAcl(target: any, prop: string | symbol | number) { +export function GetPropAcl(target: Doc | ListImpl<FieldType>, prop: string | symbol | number) { if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable return GetEffectiveAcl(target); @@ -182,7 +191,8 @@ export function GetCachedGroupByName(name: string) { export function SetCachedGroups(groups: string[]) { runInAction(() => cachedGroups.push(...groups)); } -function getEffectiveAcl(target: any, user?: string): symbol { +function getEffectiveAcl(target: Doc | ListImpl<FieldType>, user?: string): symbol { + if (target instanceof ListImpl) return AclAdmin; const targetAcls = target[DocAcl]; if (targetAcls?.acl_Me === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; @@ -287,14 +297,14 @@ export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) * @param prop * @param propSetter */ -export function SetPropSetterCb(prop: string, propSetter: ((target: any, value: any) => void) | undefined) { +export function SetPropSetterCb(prop: string, propSetter: ((target: Doc, value: FieldType) => void) | undefined) { _propSetterCB.set(prop, propSetter); } // // target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List. // -export function setter(target: any, inProp: string | symbol | number, value: any, receiver: any): boolean { +export function setter(target: ListImpl<FieldType> | Doc, inProp: string | symbol | number, value: unknown, receiver: Doc | ListImpl<FieldType>): boolean { if (!inProp) { console.log('WARNING: trying to set an empty property. This should be fixed. '); return false; @@ -303,12 +313,12 @@ export function setter(target: any, inProp: string | symbol | number, value: any const effectiveAcl = inProp === 'constructor' || typeof inProp === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't - if (typeof prop === 'string' && prop.startsWith('acl_') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; + if (typeof prop === 'string' && prop.startsWith('acl_') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value as SharingPermissions))) return true; if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { if (!prop.startsWith('__')) prop = prop.substring(1); - if (target.__LAYOUT__) { - target.__LAYOUT__[prop] = value; + if (target.__LAYOUT__ instanceof Doc) { + target.__LAYOUT__[prop] = value as FieldResult; return true; } } @@ -317,10 +327,10 @@ export function setter(target: any, inProp: string | symbol | number, value: any return !!ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success; } } - return _setter(target, prop, value, receiver); + return _setter(target, prop, value as FieldType, receiver); } -function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { +function getFieldImpl(target: ListImpl<FieldType> | Doc, prop: string | number, proxy: ListImpl<FieldType> | Doc, ignoreProto: boolean = false): FieldType { const field = target.__fieldTuples[prop]; const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys if (value) return value.value; @@ -332,7 +342,7 @@ function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProt } return field; } -export function getter(target: any, prop: string | symbol, proxy: any): any { +export function getter(target: Doc | ListImpl<FieldType>, prop: string | symbol, proxy: ListImpl<FieldType> | Doc): unknown { // prettier-ignore switch (prop) { case 'then' : return undefined; @@ -352,19 +362,23 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { } const layoutProp = prop.startsWith('_') ? prop.substring(1) : undefined; - if (layoutProp && target.__LAYOUT__) return target.__LAYOUT__[layoutProp]; + if (layoutProp && target.__LAYOUT__) return (target.__LAYOUT__ as Doc)[layoutProp]; return getFieldImpl(target, layoutProp ?? prop, proxy); } -export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { - return getFieldImpl(target, prop, target[SelfProxy], ignoreProto); +export function getField(target: ListImpl<FieldType> | Doc, prop: string | number, ignoreProto: boolean = false): unknown { + return getFieldImpl(target, prop, target[SelfProxy] as Doc, ignoreProto); } -export function deleteProperty(target: any, prop: string | number | symbol) { +export function deleteProperty(target: Doc | ListImpl<FieldType>, prop: string | number | symbol) { if (typeof prop === 'symbol') { delete target[prop]; } else { - target[SelfProxy][prop] = undefined; + if (target instanceof Doc) { + target[SelfProxy][prop] = undefined; + } else if (+prop == prop) { + target[SelfProxy].splice(+prop, 1); + } } return true; } @@ -378,39 +392,42 @@ export function deleteProperty(target: any, prop: string | number | symbol) { // were replaced. Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be // able to undo and redo the partial change. // -export function containedFieldChangedHandler(container: List<FieldType> | Doc, prop: string | number, liveContainedField: ObjectField) { - let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField; - return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any } /* , dummyServerOp?: any */) => { - const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: FieldType) => SerializationHelper.Serialize(item)) }); +export function containedFieldChangedHandler(container: ListImpl<FieldType> | Doc, prop: string | number, liveContainedField: ObjectField) { + let lastValue = ObjectField.MakeCopy(liveContainedField); + return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: (FieldType & { value?: FieldType })[] | undefined; length: number | undefined; hint?: { start: number; deleteCount: number } } /* , dummyServerOp?: any */) => { + const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: FieldType) => SerializationHelper.Serialize(item) as serializedFieldType) ?? [] }); // prettier-ignore - const serverOp = diff?.op === '$addToSet' - ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length } + const serverOp: serverOpType = diff?.op === '$addToSet' + ? { $addToSet: { ['fields.' + prop]: serializeItems(), length: diff.length ??0 }} : diff?.op === '$remFromSet' - ? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint}, length: diff.length } - : { $set: { ['fields.' + prop]: liveContainedField ? SerializationHelper.Serialize(liveContainedField) : undefined } }; + ? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint, length: diff.length ?? 0 } } + : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(liveContainedField) as {fields: serializedFieldType[]}} }; if (!(container instanceof Doc) || !container[UpdatingFromServer]) { - const prevValue = ObjectField.MakeCopy(lastValue as List<any>); + const cont = container as { [key: string | number]: FieldType }; + const prevValue = ObjectField.MakeCopy(lastValue as List<FieldType>); lastValue = ObjectField.MakeCopy(liveContainedField); const newValue = ObjectField.MakeCopy(liveContainedField); if (diff?.op === '$addToSet') { UndoManager.AddEvent( { redo: () => { + const contList = cont[prop] as List<FieldType>; // console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo - (container as any)[prop as any]?.push(...((diff.items || [])?.map((item: any) => item.value ?? item) ?? [])); - lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + contList?.push(...((diff.items || [])?.map(item => item.value ?? item) ?? [])); + lastValue = ObjectField.MakeCopy(contList); }, undo: action(() => { + const contList = cont[prop] as List<FieldType>; // console.log('undo $add: ' + prop, diff.items); // bcz: uncomment to log undo - diff.items?.forEach((item: any) => { + diff.items?.forEach(item => { const ind = item instanceof SchemaHeaderField // - ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) - : (container as any)[prop as any]?.indexOf(item.value ?? item); - ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1); + ? contList?.findIndex(ele => ele instanceof SchemaHeaderField && ele.heading === item.heading) + : contList?.indexOf(item.value ?? item); + ind !== undefined && ind !== -1 && (cont[prop] as List<FieldType>)?.splice(ind, 1); }); - lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + lastValue = ObjectField.MakeCopy(contList); }), prop: 'add ' + (diff.items?.length ?? 0) + ' items to list', }, @@ -420,48 +437,53 @@ export function containedFieldChangedHandler(container: List<FieldType> | Doc, p UndoManager.AddEvent( { redo: action(() => { + const contList = cont[prop] as List<FieldType>; // console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo - diff.items?.forEach((item: any) => { + diff.items?.forEach(item => { const ind = item instanceof SchemaHeaderField // - ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) - : (container as any)[prop as any]?.indexOf(item.value ?? item); - ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1); + ? contList?.findIndex(ele => ele instanceof SchemaHeaderField && ele.heading === item.heading) + : contList?.indexOf(item.value ?? item); + ind !== undefined && ind !== -1 && contList?.splice(ind, 1); }); - lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + lastValue = ObjectField.MakeCopy(contList); }), undo: () => { + const contList = cont[prop] as List<FieldType>; + const prevList = prevValue as List<FieldType>; // console.log('undo $rem: ' + prop, diff.items); // bcz: uncomment to log undo - diff.items?.forEach((item: any) => { + diff.items?.forEach(item => { if (item instanceof SchemaHeaderField) { - const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); - ind !== -1 && (container as any)[prop as any].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && (container as any)[prop as any].splice(ind, 0, item); + const ind = prevList.findIndex(ele => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && contList.findIndex(ele => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && contList.splice(ind, 0, item); } else { - const ind = (prevValue as List<any>).indexOf(item.value ?? item); - ind !== -1 && (container as any)[prop as any].indexOf(item.value ?? item) === -1 && (container as any)[prop as any].splice(ind, 0, item); + const ind = prevList.indexOf(item.value ?? item); + ind !== -1 && contList.indexOf(item.value ?? item) === -1 && (cont[prop] as List<FieldType>).splice(ind, 0, item); } }); - lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + lastValue = ObjectField.MakeCopy(contList); }, - prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list(' + ((container as any)?.title ?? '') + ':' + prop + ')', + prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list(' + (cont?.title ?? '') + ':' + prop + ')', }, diff?.items ); } else { const setFieldVal = (val: FieldType | undefined) => { - container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as FieldType); + container instanceof Doc ? (container[prop] = val) : (container[prop as number] = val as FieldType); }; UndoManager.AddEvent( { redo: () => { // console.log('redo list: ' + prop, fieldVal()); // bcz: uncomment to log undo - setFieldVal(newValue instanceof ObjectField ? ObjectField.MakeCopy(newValue) : undefined); - lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + setFieldVal(ObjectField.MakeCopy(newValue)); + const containerProp = cont[prop]; + if (containerProp instanceof ObjectField) lastValue = ObjectField.MakeCopy(containerProp); }, undo: () => { // console.log('undo list: ' + prop, fieldVal()); // bcz: uncomment to log undo - setFieldVal(prevValue instanceof ObjectField ? ObjectField.MakeCopy(prevValue) : undefined); - lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + setFieldVal(ObjectField.MakeCopy(prevValue)); + const containerProp = cont[prop]; + if (containerProp instanceof ObjectField) lastValue = ObjectField.MakeCopy(containerProp); }, prop: 'set list field', }, |