diff options
Diffstat (limited to 'src/fields')
-rw-r--r-- | src/fields/DateField.ts | 5 | ||||
-rw-r--r-- | src/fields/Doc.ts | 542 | ||||
-rw-r--r-- | src/fields/HtmlField.ts | 4 | ||||
-rw-r--r-- | src/fields/IconField.ts | 2 | ||||
-rw-r--r-- | src/fields/InkField.ts | 11 | ||||
-rw-r--r-- | src/fields/List.ts | 97 | ||||
-rw-r--r-- | src/fields/ObjectField.ts | 26 | ||||
-rw-r--r-- | src/fields/Proxy.ts | 17 | ||||
-rw-r--r-- | src/fields/RefField.ts | 4 | ||||
-rw-r--r-- | src/fields/RichTextUtils.ts | 210 | ||||
-rw-r--r-- | src/fields/Schema.ts | 27 | ||||
-rw-r--r-- | src/fields/SchemaHeaderField.ts | 25 | ||||
-rw-r--r-- | src/fields/ScriptField.ts | 97 | ||||
-rw-r--r-- | src/fields/Types.ts | 90 | ||||
-rw-r--r-- | src/fields/URLField.ts | 40 | ||||
-rw-r--r-- | src/fields/documentSchemas.ts | 4 | ||||
-rw-r--r-- | src/fields/util.ts | 218 |
17 files changed, 775 insertions, 644 deletions
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index 1e5222fb6..f0a851ce6 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -1,5 +1,5 @@ +import { serializable, date as serializrDate } from 'serializr'; import { Deserializable } from '../client/util/SerializationHelper'; -import { serializable, date } from 'serializr'; import { ObjectField } from './ObjectField'; import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; @@ -7,7 +7,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba @scriptingGlobal @Deserializable('date') export class DateField extends ObjectField { - @serializable(date()) + @serializable(serializrDate()) readonly date: Date; constructor(date: Date = new Date()) { @@ -38,6 +38,7 @@ export class DateField extends ObjectField { } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function d(...dateArgs: any[]) { return new DateField(new (Date as any)(...dateArgs)); }); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 48214cf25..725221a66 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,37 +1,31 @@ -import { saveAs } from 'file-saver'; +/* 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'; import { alias, map, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; import { CollectionViewType, DocumentType } from '../client/documents/DocumentTypes'; -import { LinkManager } from '../client/util/LinkManager'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; -import { undoable } from '../client/util/UndoManager'; -import { DocumentView } from '../client/views/nodes/DocumentView'; -import { decycle } from '../decycler/decycler'; -import * as JSZipUtils from '../JSZipUtils'; -import { incrementTitleCopy, Utils } from '../Utils'; -import { DateField } from './DateField'; +import { undoable, UndoManager } from '../client/util/UndoManager'; +import { ClientUtils, incrementTitleCopy } from '../ClientUtils'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks, DocAcl, DocCss, DocData, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight, Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width } from './DocSymbols'; // prettier-ignore import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -import { InkField, InkTool } from './InkField'; -import { List, ListFieldName } from './List'; +import { InkTool } from './InkField'; +import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { FieldId, RefField } from './RefField'; import { RichTextField } from './RichTextField'; import { listSpec } from './Schema'; import { ComputedField, ScriptField } from './ScriptField'; -import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; -import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; +import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types'; import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util'; -import * as JSZip from 'jszip'; -import { FieldViewProps } from '../client/views/nodes/FieldView'; + export const LinkedTo = '-linkedTo'; export namespace Field { /** @@ -43,9 +37,9 @@ export namespace Field { * @returns string representation of the field */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string { - const onDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); - const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const valFunc = (field: Field): string => { + const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const valFunc = (field: FieldType): string => { const res = field instanceof ComputedField && showComputedValue ? field.value(doc) @@ -61,9 +55,9 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (onDelegate ? '=' : '') + valFunc(field); + return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield); } - export function toScriptString(field: Field) { + export function toScriptString(field: FieldType) { switch (typeof field) { case 'string': if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s return !field.includes('`') ? `\`${field}\`` : `"${field}"`; @@ -72,8 +66,8 @@ export namespace Field { default: return field?.[ToScriptString]?.() ?? 'null'; } // prettier-ignore } - export function toJavascriptString(field: Field) { - var rawjava = ''; + export function toJavascriptString(field: FieldType) { + let rawjava = ''; switch (typeof field) { case 'string': @@ -82,12 +76,12 @@ export namespace Field { break; default: rawjava = field?.[ToJavascriptString]?.() ?? ''; } // prettier-ignore - var script = rawjava; + let script = rawjava; // this is a bit hacky, but we treat '^@' references to a published document // as a kind of macro to include the content of those documents Doc.MyPublishedDocs.forEach(doc => { const regexMultilineFlag = 'm'; - const regex = new RegExp(`^\\^${StrCast(doc.title).replace(/[\(\)]*/g, '')}\\s`, regexMultilineFlag); // need to remove characters that can cause the regular expression to be invalid + const regex = new RegExp(`^\\^${StrCast(doc.title).replace(/[()]*/g, '')}\\s`, regexMultilineFlag); // need to remove characters that can cause the regular expression to be invalid const sections = (Cast(doc.text, RichTextField, null)?.Text ?? '').split('--DOCDATA--'); if (script.match(regex)) { script = script.replace(regex, sections[0]) + (sections.length > 1 ? sections[1] : ''); @@ -95,23 +89,25 @@ export namespace Field { }); return script; } - export function toString(field: Field) { + export function toString(field: FieldType) { if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field); return field?.[ToString]?.() || ''; } - export function IsField(field: any): field is Field; - export function IsField(field: any, includeUndefined: true): field is Field | undefined; - export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined { + 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 { 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; } + UndoManager.SetFieldPrinter(toJavascriptString); } -export type Field = number | string | boolean | ObjectField | RefField; +export type FieldType = number | string | boolean | ObjectField | RefField; export type Opt<T> = T | undefined; export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>; -export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>; +export type FieldResult<T extends FieldType = FieldType> = Opt<T> | FieldWaiting<Extract<T, RefField>>; /** * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs. @@ -120,7 +116,9 @@ export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract * 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); @@ -156,19 +154,67 @@ export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol; im // caches the document access permissions for the current user. // this recursively updates all protos as well. export function updateCachedAcls(doc: Doc) { - if (!doc) return; + if (doc) { + const target = (doc as any)?.__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); + }); + if (Object.keys(permissions).length || doc[DocAcl]?.length) { + runInAction(() => { + doc[DocAcl] = permissions; + }); + } - const target = (doc as any)?.__fieldTuples ?? doc; - const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; - Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl)); - if (Object.keys(permissions).length || doc[DocAcl]?.length) { - runInAction(() => (doc[DocAcl] = permissions)); + if (doc.proto instanceof Promise) { + doc.proto.then(proto => updateCachedAcls(DocCast(proto))); + return doc.proto; + } } + return undefined; +} - if (doc.proto instanceof Promise) { - doc.proto.then(proto => updateCachedAcls(DocCast(proto))); - return doc.proto; - } +export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore +export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore +export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ''); } // prettier-ignore +export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore +export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore +export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore +export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore +export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore +export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore +export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore +export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore + +export function SetActiveInkWidth(width: string): void { + !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); +} +export function SetActiveBezierApprox(bezier: string): void { + ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); +} +export function SetActiveInkColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeInkColor = value); +} +export function SetActiveIsInkMask(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); +} +export function SetActiveInkHideTextLabels(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value); +} +export function SetActiveFillColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeFillColor = value); +} +export function SetActiveArrowStart(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); +} +export function SetActiveArrowEnd(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); +} +export function SetActiveArrowScale(value: number) { + ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); +} +export function SetActiveDash(dash: string): void { + !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); } @scriptingGlobal @@ -178,8 +224,38 @@ export class Doc extends RefField { @observable public static GuestDashboard: Doc | undefined = undefined; @observable public static GuestTarget: Doc | undefined = undefined; @observable public static GuestMobile: Doc | undefined = undefined; - public static CurrentUserEmail: string = ''; - + @observable.shallow public static CurrentlyLoading: Doc[] = observable([]); + // DocServer api + public static FindDocByTitle(title: string) { + const foundDocId = + title && + Array.from(Object.keys(DocServer.Cache())) + .filter(key => DocServer.Cache()[key] instanceof Doc) + .find(key => (DocServer.Cache()[key] as Doc).title === title); + + return foundDocId ? (DocServer.Cache()[foundDocId] as Doc) : undefined; + } + // removes from currently loading doc set + public static removeCurrentlyLoading(doc: Doc) { + if (Doc.CurrentlyLoading) { + const index = Doc.CurrentlyLoading.indexOf(doc); + runInAction(() => index !== -1 && Doc.CurrentlyLoading.splice(index, 1)); + } + } + // adds doc to currently loading display + public static addCurrentlyLoading(doc: Doc) { + if (Doc.CurrentlyLoading.indexOf(doc) === -1) { + runInAction(() => Doc.CurrentlyLoading.push(doc)); + } + } + // LinkManager api + public static AddLink: (link: Doc, checkExists?: boolean) => void; + public static DeleteLink: (link: Doc) => void; + public static Links: (link: Doc | undefined) => Doc[]; + public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined; + // KeyValue SetField + public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean; + // UserDoc "API" public static get MySharedDocs() { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore public static get MyDockedBtns() { return DocCast(Doc.UserDoc().myDockedBtns); } // prettier-ignore @@ -217,8 +293,8 @@ 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) { Doc.ActiveDashboard?.myOverlayDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore - public static RemFromMyOverlay(doc: Doc) { 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?.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 AddToMyPublished(doc: Doc) { doc[DocData].title_custom = true; doc[DocData].layout_showTitle = 'title'; @@ -274,9 +350,9 @@ export class Doc extends RefField { return Reflect.getOwnPropertyDescriptor(target, prop); } return { - configurable: true, //TODO Should configurable be true? + configurable: true, // TODO Should configurable be true? enumerable: true, - value: 0, //() => target.__fieldTuples[prop]) + value: 0, // () => target.__fieldTuples[prop]) }; }, deleteProperty: deleteProperty, @@ -288,7 +364,8 @@ export class Doc extends RefField { if (!id || forceSave) { DocServer.CreateField(docProxy); } - return 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; @@ -300,14 +377,16 @@ export class Doc extends RefField { private set __fieldTuples(value) { // called by deserializer to set all fields in one shot this[FieldTuples] = value; - for (const key in value) { - const field = value[key]; - field !== undefined && (this[FieldKeys][key] = true); - if (field instanceof ObjectField) { - field[Parent] = this[Self]; - field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + Object.keys(value).forEach(key => { + if (Object.prototype.hasOwnProperty.call(value, key)) { + const field = value[key]; + field !== undefined && (this[FieldKeys][key] = true); + if (field instanceof ObjectField) { + field[Parent] = this[Self]; + field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + } } - } + }); } @observable private [FieldTuples]: any = {}; @@ -320,7 +399,7 @@ export class Doc extends RefField { @observable public [Animation]: Opt<Doc> = undefined; @observable public [Highlight]: boolean = false; @observable public [Brushed]: boolean = false; - @observable public [DocViews] = new ObservableSet<DocumentView>(); + @observable public [DocViews] = new ObservableSet<any /* DocumentView */>(); private [Self] = this; private [SelfProxy]: any; @@ -329,7 +408,7 @@ export class Doc extends RefField { private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; public [Initializing]: boolean = false; - public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => { + public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => { if (!this[UpdatingFromServer] || this[ForceServerWrite]) { DocServer.UpdateField(this[Id], serverOp); } @@ -352,7 +431,7 @@ export class Doc extends RefField { let renderFieldKey: any; const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; if (typeof layoutField === 'string') { - renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0]; //layoutField.split("'")[1]; + [renderFieldKey] = layoutField.split("fieldKey={'")[1].split("'"); // layoutField.split("'")[1]; } else { return Cast(layoutField, Doc, null); } @@ -363,13 +442,11 @@ export class Doc extends RefField { public async [HandleUpdate](diff: any) { const set = diff.$set; - const sameAuthor = this.author === Doc.CurrentUserEmail; - if (set) { - for (const key in set) { - const fprefix = 'fields.'; - if (!key.startsWith(fprefix)) { - continue; - } + const sameAuthor = this.author === ClientUtils.CurrentUserEmail(); + const fprefix = 'fields.'; + 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]); @@ -377,7 +454,7 @@ export class Doc extends RefField { this[UpdatingFromServer] = true; this[fKey] = value; this[UpdatingFromServer] = false; - if (fKey.startsWith('acl')) { + if (fKey.startsWith('acl_')) { updateCachedAcls(this); } if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { @@ -385,20 +462,18 @@ export class Doc extends RefField { } }; const writeMode = DocServer.getFieldWriteMode(fKey); - if (fKey.startsWith('acl') || writeMode !== DocServer.WriteMode.Playground) { + 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; - if (unset) { - for (const key in unset) { - if (!key.startsWith('fields.')) { - continue; - } + Object.keys(unset ?? {}) + .filter(key => key.startsWith(fprefix)) + .forEach(async key => { const fKey = key.substring(7); const fn = () => { this[UpdatingFromServer] = true; @@ -411,12 +486,22 @@ export class Doc extends RefField { } else { this[CachedUpdates][fKey] = fn; } - } - } + }); } } +// 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; + } export function SetContainer(doc: Doc, container: Doc) { if (container !== Doc.MyRecentlyClosed) { doc.embedContainer = container; @@ -454,7 +539,7 @@ export namespace Doc { return doc; } } - export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> { + export function GetT<T extends FieldType>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>; } export function isTemplateDoc(doc: Doc) { @@ -482,8 +567,8 @@ export namespace Doc { // 2) if the data doc has the field, then it's written there. // 3) if neither already has the field, then 'defaultProto' determines whether to write it to the data doc (or the embedding) // - export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { - if (key.startsWith('_')) key = key.substring(1); + export async function SetInPlace(doc: Doc, keyIn: string, value: FieldType | undefined, defaultProto: boolean) { + const key = keyIn.startsWith('_') ? keyIn.substring(1) : keyIn; const hasProto = doc[DocData] !== doc ? doc[DocData] : undefined; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; const onProto = hasProto && Object.getOwnPropertyNames(hasProto).indexOf(key) !== -1; @@ -500,7 +585,6 @@ export namespace Doc { } return protos; } - /** * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies * the values of the properties of a source object into the target. @@ -512,17 +596,15 @@ export namespace Doc { * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key * to a potentially undefined field, where each entry in this mapping is optional. */ - export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) { + export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<FieldType>>>, skipUndefineds: boolean = false, isInitializing = false) { isInitializing && (doc[Initializing] = true); - for (const key in fields) { - if (fields.hasOwnProperty(key)) { - const value = fields[key]; - if (!skipUndefineds || value !== undefined) { - // Do we want to filter out undefineds? - doc[key] = value; - } + Object.keys(fields).forEach(key => { + const value = (fields as any)[key]; + if (!skipUndefineds || value !== undefined) { + // Do we want to filter out undefineds? + doc[key] = value; } - } + }); isInitializing && (doc[Initializing] = false); return doc; } @@ -569,7 +651,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 ? fieldKey : Doc.LayoutFieldKey(listDoc); + const key = fieldKey || Doc.LayoutFieldKey(listDoc); const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { const ind = list.indexOf(doc); @@ -586,7 +668,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 ? fieldKey : Doc.LayoutFieldKey(listDoc); + const key = fieldKey || Doc.LayoutFieldKey(listDoc); const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { if (!allowDuplicates) { @@ -603,6 +685,7 @@ export namespace Doc { if (reversed) list.splice(0, 0, doc); else list.push(doc); } else { + // eslint-disable-next-line no-lonely-if if (reversed) list.splice(before ? list.length - ind + 1 : list.length - ind, 0, doc); else list.splice(before ? ind : ind + 1, 0, doc); } @@ -634,7 +717,7 @@ export namespace Doc { embedding.createdFrom = doc; embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); - embedding.author = Doc.CurrentUserEmail; + embedding.author = ClientUtils.CurrentUserEmail(); return embedding; } @@ -642,7 +725,7 @@ export namespace Doc { export function BestEmbedding(doc: Doc) { const dataDoc = doc[DocData]; const availableEmbeddings = Doc.GetEmbeddings(dataDoc); - const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail); + const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail()); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } @@ -670,37 +753,39 @@ export namespace Doc { await Promise.all( Object.keys(doc).map(async key => { if (filter.includes(key)) return; - const assignKey = (val: any) => (copy[key] = val); + const assignKey = (val: any) => { + copy[key] = val; + }; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = async (field: ObjectField) => { + const 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(field)); - if (field instanceof RichTextField) { - if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) { + 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 = field.Data.match(docidsearch); + 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 docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key])); - docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); - rtfs.push({ copy, key, field }); + const rdocs = results && Array.from(Object.keys(results)).map(rkey => DocCast(results[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]; if (key === 'author') { - assignKey(Doc.CurrentUserEmail); + assignKey(ClientUtils.CurrentUserEmail()); } else if (docAtKey instanceof Doc) { if (pruneDocs.includes(docAtKey)) { // prune doc and do nothing @@ -709,7 +794,7 @@ export namespace Doc { (key.includes('layout[') || key.startsWith('layout') || // ['embedContainer', 'annotationOn', 'proto'].includes(key) || - (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === Doc.CurrentUserEmail)) + (['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 { @@ -722,7 +807,8 @@ export namespace Doc { } else if (field instanceof ObjectField) { await copyObjectField(field); } else if (field instanceof Promise) { - debugger; //This shouldn't happen... + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happen... } else { assignKey(field); } @@ -749,7 +835,7 @@ export namespace Doc { visited.add(clone); Object.keys(clone) .filter(key => key !== 'cloneOf') - .map(key => { + .forEach(key => { const docAtKey = DocCast(clone[key]); if (docAtKey && !Doc.IsSystem(docAtKey)) { if (!Array.from(cloneMap.values()).includes(docAtKey)) { @@ -768,16 +854,16 @@ export namespace Doc { export async 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 copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); + const clone = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); const repaired = new Set<Doc>(); const linkedDocs = Array.from(linkMap.values()); - linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true)); - rtfMap.map(({ copy, key, field }) => { - const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { + 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 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: any, href: string, id: string /* , offset: any, string: any */) => { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; @@ -786,86 +872,8 @@ export namespace Doc { copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); }); const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; - clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired)); - return { clone: copy, map: cloneMap, linkMap }; - } - - export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') { - const { clone, map, linkMap } = await Doc.MakeClone(doc); - const proms = new Set<string>(); - function replacer(key: any, value: any) { - if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined; - if (value?.__type === 'image') { - const extension = value.url.replace(/.*\./, ''); - proms.add(value.url.replace('.' + extension, '_o.' + extension)); - return SerializationHelper.Serialize(new ImageField(value.url)); - } - if (value?.__type === 'pdf') { - proms.add(value.url); - return SerializationHelper.Serialize(new PdfField(value.url)); - } - if (value?.__type === 'audio') { - proms.add(value.url); - return SerializationHelper.Serialize(new AudioField(value.url)); - } - if (value?.__type === 'video') { - proms.add(value.url); - return SerializationHelper.Serialize(new VideoField(value.url)); - } - if ( - value instanceof Doc || - value instanceof ScriptField || - value instanceof RichTextField || - value instanceof InkField || - value instanceof CsvField || - value instanceof WebField || - value instanceof DateField || - value instanceof ProxyField || - value instanceof ComputedField - ) { - return SerializationHelper.Serialize(value); - } - if (value instanceof Array && key !== ListFieldName && key !== InkField.InkDataFieldName) return { fields: value, __type: 'list' }; - return value; - } - - const docs: { [id: string]: any } = {}; - const links: { [id: string]: any } = {}; - Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1])); - Array.from(linkMap.entries()).forEach(l => (links[l[0]] = l[1])); - const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer)); - - const zip = new JSZip(); - var count = 0; - const promArr = Array.from(proms) - .filter(url => url?.startsWith('/files')) - .map(url => url.replace('/', '')); // window.location.origin)); - console.log(promArr.length); - if (!promArr.length) { - zip.file('docs.json', jsonDocs); - zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename)); - } else - promArr.forEach((url, i) => { - // loading a file and add it in a zip file - JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: any, data: any) => { - if (err) throw err; // or handle the error - // // Generate a directory within the Zip file structure - // const assets = zip.folder("assets"); - // assets.file(filename, data, {binary: true}); - const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%'); - zip.file(assetPathOnServer, data, { binary: true }); - console.log(' => ' + url); - if (++count === promArr.length) { - zip.file('docs.json', jsonDocs); - zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename)); - // const a = document.createElement("a"); - // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); - // a.href = url; - // a.download = `DocExport-${this.props.Document[Id]}.zip`; - // a.click(); - } - }); - }); + clonedDocs.forEach(cloneDoc => Doc.repairClone(cloneDoc, cloneMap, cloneTemplates, repaired)); + return { clone, map: cloneMap, linkMap }; } const _pendingMap = new Set<string>(); @@ -895,6 +903,7 @@ export namespace Doc { if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { + // eslint-disable-next-line no-param-reassign templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); @@ -905,7 +914,7 @@ export namespace Doc { newLayoutDoc.rootDocument = targetDoc; newLayoutDoc.embedContainer = targetDoc; newLayoutDoc.resolvedDataDoc = dataDoc; - newLayoutDoc['acl-Guest'] = SharingPermissions.Edit; + newLayoutDoc.acl_Guest = SharingPermissions.Edit; if (dataDoc[templateField] === undefined && (templateLayoutDoc[templateField] as any)?.length) { dataDoc[templateField] = ObjectField.MakeCopy(templateLayoutDoc[templateField] as List<Doc>); // ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"])`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); @@ -954,7 +963,7 @@ export namespace Doc { } } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { if (field instanceof Doc) { if (key === 'myLinkDatabase') { @@ -965,6 +974,7 @@ export namespace Doc { } } } else if (cfield instanceof ComputedField) { + /* empty */ } else if (field instanceof ObjectField) { if (field instanceof Doc) { Doc.FindReferences(field, references, system); @@ -981,7 +991,8 @@ export namespace Doc { Doc.FindReferences(field.value, references, system); } } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happend... } } }); @@ -1000,7 +1011,7 @@ export namespace Doc { } } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { copy[key] = field; } else if (cfield instanceof ComputedField) { @@ -1011,7 +1022,8 @@ export namespace Doc { ? new ProxyField(Doc.MakeCopy(doc[key] as any)) // copy the expanded render template : ObjectField.MakeCopy(field); } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happend... } else { copy[key] = field; } @@ -1038,10 +1050,12 @@ export namespace Doc { delegate[Initializing] = true; updateCachedAcls(delegate); delegate.proto = doc; - delegate.author = Doc.CurrentUserEmail; + delegate.author = ClientUtils.CurrentUserEmail(); Object.keys(doc) - .filter(key => key.startsWith('acl')) - .forEach(key => (delegate[key] = doc[key])); + .filter(key => key.startsWith('acl_')) + .forEach(key => { + delegate[key] = doc[key]; + }); title && (delegate.title = title); delegate[Initializing] = false; if (!Doc.IsSystem(doc)) Doc.AddEmbedding(doc, delegate); @@ -1054,7 +1068,7 @@ export namespace Doc { // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc). // This is appropriate if you're trying to create a document that behaves like all // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs) - export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string) { + export function MakeDelegateWithProto(doc: Doc /* , id?: string, title?: string */) { const ndoc = Doc.ApplyTemplate(doc); if (ndoc) { Doc.GetProto(ndoc).isDataDoc = true; @@ -1067,7 +1081,7 @@ export namespace Doc { export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { const proto = new Doc(); - proto.author = Doc.CurrentUserEmail; + proto.author = ClientUtils.CurrentUserEmail(); const target = Doc.MakeDelegate(proto); const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); @@ -1103,7 +1117,6 @@ export namespace Doc { !keepFieldKey && (templateField.title = metadataFieldKey); const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)]; - const templateCaptionValue = templateField.caption; // 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. @@ -1123,7 +1136,7 @@ export namespace Doc { // converts a document id to a url path on the server export function globalServerPath(doc: Doc | string = ''): string { - return Utils.prepend('/doc/' + (doc instanceof Doc ? doc[Id] : doc)); + return ClientUtils.prepend('/doc/' + (doc instanceof Doc ? doc[Id] : doc)); } // converts a document id to a url path on the server export function localServerPath(doc?: Doc): string { @@ -1189,7 +1202,9 @@ export namespace Doc { return manager._searchQuery; } export function SetSearchQuery(query: string) { - runInAction(() => (manager._searchQuery = query)); + runInAction(() => { + manager._searchQuery = query; + }); } export function UserDoc(): Doc { return manager._user_doc; @@ -1201,12 +1216,13 @@ export namespace Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); } export function SetUserDoc(doc: Doc) { + // eslint-disable-next-line no-return-assign return (manager._user_doc = doc); } - const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) { - return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : brushManager.SearchMatchDoc.has(doc[DocData]) ? brushManager.SearchMatchDoc.get(doc[DocData]) : undefined; - }); + const isSearchMatchCache = computedFn((doc: Doc) => + (brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : + brushManager.SearchMatchDoc.has(doc[DocData]) ? brushManager.SearchMatchDoc.get(doc[DocData]) : undefined)); // prettier-ignore export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); } @@ -1252,11 +1268,16 @@ export namespace Doc { return BrushDoc(doc, true); } export function UnBrushAllDocs() { - Array.from(brushManager.BrushedDoc).forEach(action(doc => (doc[Brushed] = false))); + Array.from(brushManager.BrushedDoc).forEach( + action(doc => { + doc[Brushed] = false; + }) + ); } - let UnhighlightWatchers: (() => void)[] = []; - export let UnhighlightTimer: any; + const UnhighlightWatchers: (() => void)[] = []; + let UnhighlightTimer: any; + export function IsUnhighlightTimerSet() { return UnhighlightTimer; } // prettier-ignore export function AddUnHighlightWatcher(watcher: () => void) { if (UnhighlightTimer) { UnhighlightWatchers.push(watcher); @@ -1270,9 +1291,9 @@ export namespace Doc { highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc)); document.removeEventListener('pointerdown', linkFollowUnhighlight); } - export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presentation_effect?: Doc) { + export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presentationEffect?: Doc) { linkFollowUnhighlight(); - (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentation_effect)); + toList(destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentationEffect)); document.removeEventListener('pointerdown', linkFollowUnhighlight); document.addEventListener('pointerdown', linkFollowUnhighlight); if (UnhighlightTimer) clearTimeout(UnhighlightTimer); @@ -1282,31 +1303,34 @@ export namespace Doc { }, 5000); } - export var highlightedDocs = new ObservableSet<Doc>(); + export const highlightedDocs = new ObservableSet<Doc>(); export function IsHighlighted(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(doc[DocData]) === AclPrivate || doc.opacity === 0) return false; return doc[Highlight] || doc[DocData][Highlight]; } - export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presentation_effect?: Doc) { + export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presentationEffect?: Doc) { runInAction(() => { highlightedDocs.add(doc); doc[Highlight] = true; - doc[Animation] = presentation_effect; + doc[Animation] = presentationEffect; if (dataAndDisplayDocs && !doc.resolvedDataDoc) { // if doc is a layout template then we don't want to highlight the proto since that will be the entire template, not just the specific layout field highlightedDocs.add(doc[DocData]); doc[DocData][Highlight] = true; + // want to highlight the targets of presentation docs explicitly since following a pres target does not highlight PDf <Annotations> which are not DocumentViews + if (doc.presentation_targetDoc) DocCast(doc.presentation_targetDoc)[Highlight] = true; } }); } /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted - export function UnHighlightDoc(doc?: Doc) { + export function UnHighlightDoc(docs?: Doc) { runInAction(() => { - (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => { + (docs ? [docs] : Array.from(highlightedDocs)).forEach(doc => { highlightedDocs.delete(doc); highlightedDocs.delete(doc[DocData]); doc[Highlight] = doc[DocData][Highlight] = false; doc[Animation] = undefined; + if (doc.presentation_targetDoc) DocCast(doc.presentation_targetDoc)[Highlight] = false; }); }); } @@ -1380,6 +1404,7 @@ export namespace Doc { 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()) { + // eslint-disable-next-line no-param-reassign if (toggle) modifiers = 'remove'; else return; } @@ -1404,9 +1429,12 @@ export namespace Doc { return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])]; } } + return undefined; } export function assignDocToField(doc: Doc, field: string, id: string) { - DocServer.GetRefField(id).then(layout => layout instanceof Doc && (doc[field] = layout)); + DocServer.GetRefField(id).then(layout => { + layout instanceof Doc && (doc[field] = layout); + }); return id; } @@ -1426,20 +1454,6 @@ export namespace Doc { }); } - export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: number) { - const style: { [key: string]: any } = {}; - const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; - const replacer = (match: any, expr: string, offset: any, string: any) => { - // bcz: this executes a script to convert a property expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? ''; - }; - divKeys.map((prop: string) => { - const p = (props as any)[prop]; - typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); - }); - return style; - } - export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) { DocServer.GetRefFields(docids).then(async fieldlist => { const list = Array.from(Object.values(fieldlist)) @@ -1495,6 +1509,7 @@ export namespace Doc { case DocumentType.EQUATION: return 'calculator'; case DocumentType.SIMULATION: return 'rocket'; case DocumentType.CONFIG: return 'folder-closed'; + default: } return 'question'; } @@ -1505,7 +1520,7 @@ export namespace Doc { // If they are not remapped, loading the file will overwrite any existing documents with those ids // export async function importDocument(file: File, remap = false) { - const upload = Utils.prepend('/uploadDoc'); + const upload = ClientUtils.prepend('/uploadDoc'); const formData = new FormData(); if (file) { formData.append('file', file); @@ -1513,12 +1528,13 @@ export namespace Doc { const response = await fetch(upload, { method: 'POST', body: formData }); const json = await response.json(); if (json !== 'error') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const docs = await DocServer.GetRefFields(json.docids as string[]); const doc = DocCast(await DocServer.GetRefField(json.id)); const links = await DocServer.GetRefFields(json.linkids as string[]); Array.from(Object.keys(links)) .map(key => links[key]) - .forEach(link => link instanceof Doc && LinkManager.Instance.addLink(link)); + .forEach(link => link instanceof Doc && Doc.AddLink?.(link)); return doc; } } @@ -1578,6 +1594,7 @@ export namespace Doc { */ export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> { if (excludeEmptyObjects === undefined) { + // eslint-disable-next-line no-param-reassign excludeEmptyObjects = true; } if (data === undefined || data === null || ![...primitives, 'object'].includes(typeof data)) { @@ -1617,12 +1634,12 @@ export namespace Doc { if (hasEntries || !excludeEmptyObjects) { const resolved = target ?? new Doc(); if (hasEntries) { - let result: Opt<Field>; - Object.keys(object).map(key => { + Object.keys(object).forEach(key => { // if excludeEmptyObjects is true, any qualifying conversions from toField will // be undefined, and thus the results that would have // otherwise been empty (List or Doc)s will just not be written - if ((result = toField(object[key], excludeEmptyObjects, key))) { + const result = toField(object[key], excludeEmptyObjects, key); + if (result) { resolved[key] = result; } }); @@ -1630,6 +1647,7 @@ export namespace Doc { title && (resolved.title = title); return resolved; } + return undefined; }; /** @@ -1639,19 +1657,22 @@ 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<Field>> => { + const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<FieldType>> => { const target = new List(); - let result: Opt<Field>; + let result: Opt<FieldType>; // if excludeEmptyObjects is true, any qualifying conversions from toField will // be undefined, and thus the results that would have // otherwise been empty (List or Doc)s will just not be written - list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result)); + list.forEach(item => { + (result = toField(item, excludeEmptyObjects)) && target.push(result); + }); if (target.length || !excludeEmptyObjects) { return target; } + return undefined; }; - const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => { + const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<FieldType> => { if (data === null || data === undefined) { return undefined; } @@ -1666,61 +1687,102 @@ export namespace 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; + } + return ''; +} +export function GetDocFromUrl(url: string) { + return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId +} + +let activeAudioLinker: (f: () => Doc | undefined, broadcast?: boolean) => (Doc | undefined)[]; +export function SetActiveAudioLinker(func: (f: () => Doc | undefined, broadcast?: boolean) => (Doc | undefined)[]) { + activeAudioLinker = func; +} +export function CreateLinkToActiveAudio(func: () => Doc | undefined, broadcast?: boolean) { + return activeAudioLinker(func, broadcast); +} + export function IdToDoc(id: string) { return DocCast(DocServer.GetCachedRefField(id)); } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function idToDoc(id: string): any { return IdToDoc(id); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renameEmbedding(doc: any) { return StrCast(doc[DocData].title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getProto(doc: any) { return Doc.GetProto(doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getEmbedding(doc: any) { return Doc.MakeEmbedding(doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function copyField(field: any) { return Field.Copy(field); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function docList(field: any) { return DocListCast(field); }); +// eslint-disable-next-line prefer-arrow-callback 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) { return Doc.SetInPlace(doc, field, value, false); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); }); +// eslint-disable-next-line prefer-arrow-callback 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 { return Cast(doc, 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)]; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: 'match' | 'check' | 'x' | 'remove') { Doc.setDocFilter(container, key, value, modifiers); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function toJavascriptString(str: string) { - return Field.toJavascriptString(str as Field); + return Field.toJavascriptString(str as FieldType); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function RtfField() { return RichTextField.RTFfield(); }); diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts index b67f0f7e9..536f5ce4c 100644 --- a/src/fields/HtmlField.ts +++ b/src/fields/HtmlField.ts @@ -1,7 +1,7 @@ +import { primitive, serializable } from 'serializr'; import { Deserializable } from '../client/util/SerializationHelper'; -import { serializable, primitive } from 'serializr'; -import { ObjectField } from './ObjectField'; import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; +import { ObjectField } from './ObjectField'; @Deserializable('html') export class HtmlField extends ObjectField { diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts index 4d2badb68..33e5be7af 100644 --- a/src/fields/IconField.ts +++ b/src/fields/IconField.ts @@ -1,5 +1,5 @@ -import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, primitive } from 'serializr'; +import { Deserializable } from '../client/util/SerializationHelper'; import { ObjectField } from './ObjectField'; import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index b3e01229a..c4f5f7a24 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -2,6 +2,7 @@ import { Bezier } from 'bezier-js'; import { alias, createSimpleSchema, list, object, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; +import { PointData } from '../pen-gestures/GestureTypes'; import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; @@ -16,12 +17,6 @@ export enum InkTool { PresentationPin = 'presentationpin', } -// Defines a point in an ink as a pair of x- and y-coordinates. -export interface PointData { - X: number; - Y: number; -} - export type Segment = Array<Bezier>; // Defines an ink as an array of points. @@ -62,10 +57,10 @@ const strokeDataSchema = createSimpleSchema({ '*': true, }); +export const InkDataFieldName = '__inkData'; @Deserializable('ink') export class InkField extends ObjectField { - public static InkDataFieldName = '__inkData'; - @serializable(alias(InkField.InkDataFieldName, list(object(strokeDataSchema)))) + @serializable(alias(InkDataFieldName, list(object(strokeDataSchema)))) readonly inkData: InkData; constructor(data: InkData) { diff --git a/src/fields/List.ts b/src/fields/List.ts index ec31f7dae..38c47d546 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -1,31 +1,28 @@ import { action, computed, makeObservable, observable } from 'mobx'; -import { alias, list, serializable } from 'serializr'; -import { DocServer } from '../client/DocServer'; +import { alias, list as serializrList, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper'; -import { Field } from './Doc'; +import { Field, FieldType, StrListCast } from './Doc'; import { FieldTuples, Self, SelfProxy } from './DocSymbols'; import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -import { ObjectField } from './ObjectField'; +import { ObjGetRefFields, ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; -import { listSpec } from './Schema'; -import { Cast } from './Types'; import { containedFieldChangedHandler, deleteProperty, getter, setter } from './util'; -function toObjectField(field: Field) { +function toObjectField(field: FieldType) { return field instanceof RefField ? new ProxyField(field) : field; } -function toRealField(field: Field) { +function toRealField(field: FieldType) { return field instanceof ProxyField ? field.value : field; } -type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T; +type StoredType<T extends FieldType> = T extends RefField ? ProxyField<T> : T; export const ListFieldName = 'fields'; @Deserializable('list') -class ListImpl<T extends Field> extends ObjectField { +class ListImpl<T extends FieldType> extends ObjectField { static listHandlers: any = { /// Mutator methods copyWithin() { @@ -44,14 +41,14 @@ class ListImpl<T extends Field> extends ObjectField { this[SelfProxy][FieldChanged]?.(); return field; }, - push: action(function (this: ListImpl<any>, ...items: any[]) { - items = items.map(toObjectField); + push: action(function (this: ListImpl<any>, ...itemsIn: any[]) { + const items = itemsIn.map(toObjectField); const list = this[Self]; - const length = list.__fieldTuples.length; + const { length } = list.__fieldTuples; for (let i = 0; i < items.length; i++) { const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist + // TODO Error checking to make sure parent doesn't already exist if (item instanceof ObjectField) { item[Parent] = list; item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], i + length, item); @@ -77,21 +74,21 @@ class ListImpl<T extends Field> extends ObjectField { this[SelfProxy][FieldChanged]?.(); return res; }, - splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { + splice: action(function (this: any, start: number, deleteCount: number, ...itemsIn: any[]) { this[Self].__realFields; // coerce retrieving entire array - items = items.map(toObjectField); + const items = itemsIn.map(toObjectField); const list = this[Self]; const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount); for (let i = 0; i < items.length; i++) { const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist - //TODO Need to change indices of other fields in array + // TODO Error checking to make sure parent doesn't already exist + // TODO Need to change indices of other fields in array if (item instanceof ObjectField) { item[Parent] = list; item[FieldChanged] = containedFieldChangedHandler(this, i + start, item); } } - let hintArray: { val: any; index: number }[] = []; + const hintArray: { val: any; index: number }[] = []; for (let i = start; i < start + deleteCount; i++) { hintArray.push({ val: list.__fieldTuples[i], index: i }); } @@ -107,13 +104,13 @@ class ListImpl<T extends Field> extends ObjectField { ); return res.map(toRealField); }), - unshift(...items: any[]) { - items = items.map(toObjectField); + unshift(...itemsIn: any[]) { + const items = itemsIn.map(toObjectField); const list = this[Self]; for (let i = 0; i < items.length; i++) { const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist - //TODO Need to change indices of other fields in array + // TODO Error checking to make sure parent doesn't already exist + // TODO Need to change indices of other fields in array if (item instanceof ObjectField) { item[Parent] = list; item[FieldChanged] = containedFieldChangedHandler(this, i, item); @@ -131,9 +128,8 @@ class ListImpl<T extends Field> extends ObjectField { includes(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields.includes(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.includes(valueToFind, fromIndex); } + return this[Self].__fieldTuples.includes(valueToFind, fromIndex); }, indexOf(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { @@ -151,9 +147,8 @@ class ListImpl<T extends Field> extends ObjectField { lastIndexOf(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields.lastIndexOf(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); } + return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); }, slice(begin: number, end: number) { this[Self].__realFields; @@ -226,7 +221,7 @@ class ListImpl<T extends Field> extends ObjectField { }, }; static listGetter(target: any, prop: string | symbol, receiver: any): any { - if (ListImpl.listHandlers.hasOwnProperty(prop)) { + if (Object.prototype.hasOwnProperty.call(ListImpl.listHandlers, prop)) { return ListImpl.listHandlers[prop]; } return getter(target, prop, receiver); @@ -244,7 +239,7 @@ class ListImpl<T extends Field> extends ObjectField { getOwnPropertyDescriptor: (target, prop) => { if (prop in target[FieldTuples]) { return { - configurable: true, //TODO Should configurable be true? + configurable: true, // TODO Should configurable be true? enumerable: true, }; } @@ -255,11 +250,13 @@ class ListImpl<T extends Field> extends ObjectField { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); - this[SelfProxy] = list as any as List<Field>; // bcz: ugh .. don't know how to convince typesecript that list is a List + // 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 if (fields) { this[SelfProxy].push(...fields); } - return list; + // 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 } [key: number]: T | (T extends RefField ? Promise<T> : never); @@ -271,7 +268,7 @@ class ListImpl<T extends Field> extends ObjectField { // 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 = DocServer.GetRefFields(unrequested.map(p => p.fieldId)); + 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])))); @@ -282,20 +279,20 @@ class ListImpl<T extends Field> extends ObjectField { return this[FieldTuples].map(toRealField); } - @serializable(alias(ListFieldName, list(autoObject(), { afterDeserialize: afterDocDeserialize }))) + @serializable(alias(ListFieldName, serializrList(autoObject(), { afterDeserialize: afterDocDeserialize }))) private get __fieldTuples() { return this[FieldTuples]; } private set __fieldTuples(value) { this[FieldTuples] = value; - for (const key in value) { - const item = value[key]; + Object.keys(value).forEach(key => { + const item = value[Number(key)]; if (item instanceof ObjectField) { item[Parent] = this[Self]; item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item); } - } + }); } [Copy]() { @@ -308,24 +305,24 @@ class ListImpl<T extends Field> extends ObjectField { @observable private [FieldTuples]: StoredType<T>[] = []; private [Self] = this; - private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references + // eslint-disable-next-line no-use-before-define + private [SelfProxy]: List<FieldType>; // also used in utils.ts even though it won't be found using find all references - [ToJavascriptString]() { - return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; - } - [ToScriptString]() { - return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; - } - [ToString]() { - return `[${(this as any).map((field: any) => Field.toString(field))}]`; - } + [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 } -export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; -export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; + +// 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>() +// eslint-disable-next-line no-redeclare +export const List: { new <T extends FieldType>(fields?: T[]): List<T> } = ListImpl as any; ScriptingGlobals.add('List', List); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function compareLists(l1: any, l2: any) { - const L1 = Cast(l1, listSpec('string'), []); - const L2 = Cast(l2, listSpec('string'), []); + 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); }, 'compare two lists'); diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index e1b5b036c..231086262 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -1,26 +1,36 @@ -import { RefField } from './RefField'; -import { FieldChanged, Parent, Copy, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; -import { Field } from './Doc'; +import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; +import { RefField } from './RefField'; export abstract class ObjectField { // prettier-ignore public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; - items: Field[] | undefined; + // eslint-disable-next-line no-use-before-define + items: FieldType[] | undefined; length: number | undefined; hint?: any }, serverOp?: any) => void; + // eslint-disable-next-line no-use-before-define public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; abstract [ToJavascriptString](): string; abstract [ToScriptString](): string; abstract [ToString](): string; -} - -export namespace ObjectField { - export function MakeCopy<T extends ObjectField>(field: T) { + static MakeCopy<T extends ObjectField>(field: T) { return field?.[Copy](); } } +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 820d9b6ff..4f8058ce4 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -21,7 +21,7 @@ export class ProxyField<T extends RefField> extends ObjectField { constructor(value?: T | string) { super(); if (typeof value === 'string') { - //this.cache = DocServer.GetCachedRefField(value) as any; + // this.cache = DocServer.GetCachedRefField(value) as any; this.fieldId = value; } else if (value) { this.cache = { field: value, p: undefined }; @@ -29,7 +29,7 @@ export class ProxyField<T extends RefField> extends ObjectField { } } - [ToValue](doc: any) { + [ToValue](/* doc: any */) { return ProxyField.toValue(this); } @@ -39,10 +39,10 @@ export class ProxyField<T extends RefField> extends ObjectField { } [ToJavascriptString]() { - return Field.toScriptString(this[ToValue](undefined)?.value); + return Field.toScriptString(this[ToValue]()?.value); } [ToScriptString]() { - return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? + return Field.toScriptString(this[ToValue]()?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? } [ToString]() { return 'ProxyField'; @@ -59,7 +59,9 @@ export class ProxyField<T extends RefField> extends ObjectField { return this._cache; } private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) { - runInAction(() => (this._cache = { ...val })); + runInAction(() => { + this._cache = { ...val }; + }); } private failed = false; @@ -78,7 +80,7 @@ export class ProxyField<T extends RefField> extends ObjectField { return this.cache.field ?? this.cache.p; } @computed get needsRequesting(): boolean { - return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false; + return !!(!this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId)); } setExternalValuePromise(externalValuePromise: Promise<any>) { @@ -92,6 +94,7 @@ export class ProxyField<T extends RefField> extends ObjectField { } } +// eslint-disable-next-line no-redeclare export namespace ProxyField { let useProxy = true; export function DisableProxyFields() { @@ -115,9 +118,11 @@ export namespace ProxyField { if (useProxy) { return { value: value.value }; } + return undefined; } } +// eslint-disable-next-line no-use-before-define function prefetchValue(proxy: PrefetchProxy<RefField>) { return proxy.value as any; } diff --git a/src/fields/RefField.ts b/src/fields/RefField.ts index 01828dd14..1ce81368a 100644 --- a/src/fields/RefField.ts +++ b/src/fields/RefField.ts @@ -1,7 +1,7 @@ -import { serializable, primitive, alias } from 'serializr'; +import { alias, primitive, serializable } from 'serializr'; import { Utils } from '../Utils'; -import { Id, HandleUpdate, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols'; import { afterDocDeserialize } from '../client/util/SerializationHelper'; +import { HandleUpdate, Id, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; export type FieldId = string; export abstract class RefField { diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index b84a91709..5eb60a2f8 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -1,21 +1,25 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-use-before-define */ import { AssertionError } from 'assert'; -import { docs_v1 } from 'googleapis'; +import * as Color from 'color'; +import { docs_v1 as docsV1 } from 'googleapis'; import { Fragment, Mark, Node } from 'prosemirror-model'; import { sinkListItem } from 'prosemirror-schema-list'; import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; -import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils'; -import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils'; +import { ClientUtils, DashColor } from '../ClientUtils'; +import { Utils } from '../Utils'; import { DocServer } from '../client/DocServer'; -import { Docs, DocUtils } from '../client/documents/Documents'; import { Networking } from '../client/Network'; +import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils'; +import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils'; +import { Docs } from '../client/documents/Documents'; +import { DocUtils } from '../client/documents/DocUtils'; import { FormattedTextBox } from '../client/views/nodes/formattedText/FormattedTextBox'; import { schema } from '../client/views/nodes/formattedText/schema_rts'; -import { DashColor, Utils } from '../Utils'; import { Doc, Opt } from './Doc'; import { Id } from './FieldSymbols'; import { RichTextField } from './RichTextField'; import { Cast, StrCast } from './Types'; -import * as Color from 'color'; export namespace RichTextUtils { const delimiter = '\n'; @@ -47,13 +51,11 @@ export namespace RichTextUtils { return JSON.stringify(state); }; - export const Synthesize = (plainText: string, oldState?: RichTextField) => { - return new RichTextField(ToProsemirrorState(plainText, oldState), plainText); - }; + export const Synthesize = (plainText: string, oldState?: RichTextField) => new RichTextField(ToProsemirrorState(plainText, oldState), plainText); export const ToPlainText = (state: EditorState) => { // Because we're working with plain text, just concatenate all paragraphs - const content = state.doc.content; + const { content } = state.doc; const paragraphs: Node[] = []; content.forEach(node => node.type.name === 'paragraph' && paragraphs.push(node)); @@ -112,9 +114,9 @@ export namespace RichTextUtils { agnostic: string; } - const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise<Map<string, ImageTemplate>> => { + const parseInlineObjects = async (document: docsV1.Schema$Document): Promise<Map<string, ImageTemplate>> => { const inlineObjectMap = new Map<string, ImageTemplate>(); - const inlineObjects = document.inlineObjects; + const { inlineObjects } = document; if (inlineObjects) { const objects = Object.keys(inlineObjects).map(objectId => inlineObjects[objectId]); @@ -140,8 +142,8 @@ export namespace RichTextUtils { inlineObjectMap.set(object.objectId!, { title: embeddedObject.title || `Imported Image from ${document.title}`, width, - url: Utils.prepend(_m.client), - agnostic: Utils.prepend(agnostic.client), + url: ClientUtils.prepend(_m.client), + agnostic: ClientUtils.prepend(agnostic.client), }); } } @@ -170,10 +172,12 @@ export namespace RichTextUtils { const indentMap = new Map<ListGroup, BulletPosition[]>(); let globalOffset = 0; const nodes: Node[] = []; + // eslint-disable-next-line no-restricted-syntax for (const element of structured) { if (Array.isArray(element)) { lists.push(element); const positions: BulletPosition[] = []; + // eslint-disable-next-line no-loop-func const items = element.map(paragraph => { const item = listItem(state.schema, paragraph.contents); const sinks = paragraph.bullet!; @@ -187,41 +191,42 @@ export namespace RichTextUtils { }); indentMap.set(element, positions); nodes.push(list(state.schema, items)); + } else if (element.contents.some(child => 'inlineObjectId' in child)) { + const group = element.contents; + // eslint-disable-next-line no-loop-func + group.forEach((child, i) => { + let node: Opt<Node>; + if ('inlineObjectId' in child) { + node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote); + } else if ('content' in child && (i !== group.length - 1 || child.content!.removeTrailingNewlines().length)) { + node = paragraphNode(state.schema, [child]); + } + if (node) { + position += node.nodeSize; + nodes.push(node); + } + }); } else { - if (element.contents.some(child => 'inlineObjectId' in child)) { - const group = element.contents; - group.forEach((child, i) => { - let node: Opt<Node>; - if ('inlineObjectId' in child) { - node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote); - } else if ('content' in child && (i !== group.length - 1 || child.content!.removeTrailingNewlines().length)) { - node = paragraphNode(state.schema, [child]); - } - if (node) { - position += node.nodeSize; - nodes.push(node); - } - }); - } else { - const paragraph = paragraphNode(state.schema, element.contents); - nodes.push(paragraph); - position += paragraph.nodeSize; - } + const paragraph = paragraphNode(state.schema, element.contents); + nodes.push(paragraph); + position += paragraph.nodeSize; } } state = state.apply(state.tr.replaceWith(0, 2, nodes)); const sink = sinkListItem(state.schema.nodes.list_item); - const dispatcher = (tr: Transaction) => (state = state.apply(tr)); - for (const list of lists) { - for (const pos of indentMap.get(list)!) { + const dispatcher = (tr: Transaction) => { + state = state.apply(tr); + }; + lists.forEach(list => { + indentMap.get(list)?.forEach(pos => { const resolved = state.doc.resolve(pos.value); state = state.apply(state.tr.setSelection(new TextSelection(resolved))); for (let i = 0; i < pos.sinks; i++) { sink(state, dispatcher); } - } - } + }); + }); return { title, text, state }; }; @@ -233,7 +238,7 @@ export namespace RichTextUtils { const parseLists = (paragraphs: ListGroup) => { const groups: PreparedParagraphs = []; let group: ListGroup = []; - for (const paragraph of paragraphs) { + paragraphs.forEach(paragraph => { if (paragraph.bullet !== undefined) { group.push(paragraph); } else { @@ -243,26 +248,22 @@ export namespace RichTextUtils { } groups.push(paragraph); } - } + }); group.length && groups.push(group); return groups; }; - const listItem = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => { - return schema.node('list_item', null, paragraphNode(schema, runs)); - }; + const listItem = (lschema: any, runs: docsV1.Schema$TextRun[]): Node => lschema.node('list_item', null, paragraphNode(lschema, runs)); - const list = (schema: any, items: Node[]): Node => { - return schema.node('ordered_list', { mapStyle: 'bullet' }, items); - }; + const list = (lschema: any, items: Node[]): Node => lschema.node('ordered_list', { mapStyle: 'bullet' }, items); - const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => { - const children = runs.map(run => textNode(schema, run)).filter(child => child !== undefined); + const paragraphNode = (lschema: any, runs: docsV1.Schema$TextRun[]): Node => { + const children = runs.map(run => textNode(lschema, run)).filter(child => child !== undefined); const fragment = children.length ? Fragment.from(children) : undefined; - return schema.node('paragraph', null, fragment); + return lschema.node('paragraph', null, fragment); }; - const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => { + const imageNode = (lschema: any, image: ImageTemplate, textNote: Doc) => { const { url: src, width, agnostic } = image; let docId: string; const guid = Utils.GenerateDeterministicGuid(agnostic); @@ -275,30 +276,30 @@ export namespace RichTextUtils { } else { docId = backingDocId; } - return schema.node('image', { src, agnostic, width, docId, float: null }); + return lschema.node('image', { src, agnostic, width, docId, float: null }); }; - const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { + const textNode = (lschema: any, run: docsV1.Schema$TextRun) => { const text = run.content!.removeTrailingNewlines(); - return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined; + return text.length ? lschema.text(text, styleToMarks(lschema, run.textStyle)) : undefined; }; - const StyleToMark = new Map<keyof docs_v1.Schema$TextStyle, keyof typeof schema.marks>([ + const StyleToMark = new Map<keyof docsV1.Schema$TextStyle, keyof typeof schema.marks>([ ['bold', 'strong'], ['italic', 'em'], ['foregroundColor', 'pFontColor'], ['fontSize', 'pFontSize'], ]); - const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => { + const styleToMarks = (lschema: any, textStyle?: docsV1.Schema$TextStyle) => { if (!textStyle) { return undefined; } const marks: Mark[] = []; Object.keys(textStyle).forEach(key => { - let value: any; - const targeted = key as keyof docs_v1.Schema$TextStyle; - if ((value = textStyle[targeted])) { + const targeted = key as keyof docsV1.Schema$TextStyle; + const value = textStyle[targeted] as any; + if (value) { const attributes: any = {}; let converted = StyleToMark.get(targeted) || targeted; @@ -315,20 +316,20 @@ export namespace RichTextUtils { converted = ImportFontFamilyMapping.get(value.fontFamily) || 'timesNewRoman'; } - const mapped = schema.marks[converted]; + const mapped = lschema.marks[converted]; if (!mapped) { alert(`No mapping found for ${converted}!`); return; } - const mark = schema.mark(mapped, attributes); + const mark = lschema.mark(mapped, attributes); mark && marks.push(mark); } }); return marks; }; - const MarkToStyle = new Map<keyof typeof schema.marks, keyof docs_v1.Schema$TextStyle>([ + const MarkToStyle = new Map<keyof typeof schema.marks, keyof docsV1.Schema$TextStyle>([ ['strong', 'bold'], ['em', 'italic'], ['pFontColor', 'foregroundColor'], @@ -360,16 +361,18 @@ export namespace RichTextUtils { const ignored = ['user_mark']; - const marksToStyle = async (nodes: (Node | null)[]): Promise<docs_v1.Schema$Request[]> => { - const requests: docs_v1.Schema$Request[] = []; + const marksToStyle = async (nodes: (Node | null)[]): Promise<docsV1.Schema$Request[]> => { + const requests: docsV1.Schema$Request[] = []; let position = 1; + // eslint-disable-next-line no-restricted-syntax for (const node of nodes) { if (node === null) { position += 2; + // eslint-disable-next-line no-continue continue; } const { marks, attrs, nodeSize } = node; - const textStyle: docs_v1.Schema$TextStyle = {}; + const textStyle: docsV1.Schema$TextStyle = {}; const information: LinkInformation = { startIndex: position, endIndex: position + nodeSize, @@ -377,36 +380,45 @@ export namespace RichTextUtils { }; let mark: Mark; const markMap = BuildMarkMap(marks); + // eslint-disable-next-line no-restricted-syntax 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 docs_v1.Schema$TextStyle); + let converted = MarkToStyle.get(markName) || (markName as keyof docsV1.Schema$TextStyle); let value: any = true; if (!converted) { + // eslint-disable-next-line no-continue continue; } + // eslint-disable-next-line @typescript-eslint/no-shadow const { attrs } = mark; switch (converted) { case 'link': - let url = attrs.allLinks.length ? attrs.allLinks[0].href : ''; - const delimiter = '/doc/'; - const alreadyShared = '?sharing=true'; - if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { - const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]); - if (linkDoc instanceof Doc) { - let exported = (await Cast(linkDoc.link_anchor_2, Doc))!; - if (!exported.customLayout) { - exported = Doc.MakeEmbedding(exported); - DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument); - linkDoc.link_anchor_2 = exported; + { + let url = attrs.allLinks.length ? attrs.allLinks[0].href : ''; + 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); + DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument); + linkDoc.link_anchor_2 = exported; + } + url = ClientUtils.shareUrl(exported[Id]); } - url = Utils.shareUrl(exported[Id]); } + value = { url }; + textStyle.foregroundColor = fromRgb.blue; + textStyle.bold = true; } - value = { url }; - textStyle.foregroundColor = fromRgb.blue; - textStyle.bold = true; break; case 'fontSize': value = { magnitude: attrs.fontSize, unit: 'PT' }; @@ -416,9 +428,11 @@ export namespace RichTextUtils { break; case 'weightedFontFamily': value = { fontFamily: ExportFontFamilyMapping.get(markName) }; + break; + default: } - let matches: RegExpExecArray | null; - if ((matches = /p(\d+)/g.exec(markName)) !== null) { + const matches = /p(\d+)/g.exec(markName); + if (matches !== null) { converted = 'fontSize'; value = { magnitude: parseInt(matches[1].replace('px', '')), unit: 'PT' }; } @@ -428,7 +442,7 @@ export namespace RichTextUtils { requests.push(EncodeStyleUpdate(information)); } if (node.type.name === 'image') { - const width = attrs.width; + const { width } = attrs; requests.push( await EncodeImage({ startIndex: position + nodeSize - 1, @@ -444,14 +458,16 @@ export namespace RichTextUtils { const BuildMarkMap = (marks: readonly Mark[]) => { const markMap: { [type: string]: Mark } = {}; - marks.forEach(mark => (markMap[mark.type.name] = mark)); + marks.forEach(mark => { + markMap[mark.type.name] = mark; + }); return markMap; }; interface LinkInformation { startIndex: number; endIndex: number; - textStyle: docs_v1.Schema$TextStyle; + textStyle: docsV1.Schema$TextStyle; } interface ImageInformation { @@ -461,36 +477,34 @@ export namespace RichTextUtils { } namespace fromRgb { - export const convert = (red: number, green: number, blue: number): docs_v1.Schema$OptionalColor => { - return { - color: { - rgbColor: { - red: red / 255, - green: green / 255, - blue: blue / 255, - }, + export const convert = (red: number, green: number, blue: number): docsV1.Schema$OptionalColor => ({ + color: { + rgbColor: { + red: red / 255, + green: green / 255, + blue: blue / 255, }, - }; - }; + }, + }); export const red = convert(255, 0, 0); export const green = convert(0, 255, 0); export const blue = convert(0, 0, 255); } - const fromHex = (color: string): docs_v1.Schema$OptionalColor => { + const fromHex = (color: string): docsV1.Schema$OptionalColor => { const c = DashColor(color); return fromRgb.convert(c.red(), c.green(), c.blue()); }; - const EncodeStyleUpdate = (information: LinkInformation): docs_v1.Schema$Request => { + const EncodeStyleUpdate = (information: LinkInformation): docsV1.Schema$Request => { const { startIndex, endIndex, textStyle } = information; return { updateTextStyle: { fields: '*', range: { startIndex, endIndex }, textStyle, - } as docs_v1.Schema$UpdateTextStyleRequest, + } as docsV1.Schema$UpdateTextStyleRequest, }; }; diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index f5e64ae1f..89e5cda8d 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -1,5 +1,9 @@ +/* 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, Field } from './Doc'; +import { Doc, FieldType } from './Doc'; import { ObjectField } from './ObjectField'; import { RefField } from './RefField'; import { SelfProxy } from './DocSymbols'; @@ -12,6 +16,7 @@ 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 +41,10 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu if (prop in schema) { const desc = prop === 'proto' ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? if (typeof desc === 'object' && 'defaultVal' in desc && 'type' in desc) { - //defaultSpec + // defaultSpec return Cast(field, desc.type, desc.defaultVal); } + // eslint-disable-next-line no-prototype-builtins if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { const doc = Cast(field, Doc); if (doc === undefined) { @@ -47,7 +53,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu if (doc instanceof Doc) { return desc(doc); } - return doc.then(doc => doc && desc(doc)); + return doc.then(d => d && desc(d)); } return Cast(field, desc); } @@ -73,8 +79,8 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) get() { return Cast(this.__doc[key], type as any); }, - set(value) { - value = Cast(value, type as any); + set(setValue) { + const value = Cast(setValue, type as any); if (value !== undefined) { this.__doc[key] = value; return; @@ -93,17 +99,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; - (schema as any).proto = Doc; - return schema as any; + // (schema as any).proto = Doc; + // return schema as any; } -export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> { - return { List: type as any }; //TODO Types +export function listSpec<U extends ToConstructor<FieldType>>(type: U): ListSpec<ToType<U>> { + return { List: type as any }; // TODO Types } -export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> { +export function defaultSpec<T extends ToConstructor<FieldType>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> { return { type: type as any, defaultVal, diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index fb4dc4e5b..0a8dd1d9e 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,9 +1,19 @@ +import { primitive, serializable } from 'serializr'; +import { ScriptingGlobals, scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { serializable, primitive } from 'serializr'; +import { Copy, FieldChanged, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString, FieldChanged, ToJavascriptString } from './FieldSymbols'; -import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; -import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; + +export enum ColumnType { + Number, + String, + Boolean, + Date, + Image, + RTF, + Enumeration, + Any, +} export const PastelSchemaPalette = new Map<string, string>([ // ["pink1", "#FFB4E8"], @@ -69,13 +79,14 @@ export class SchemaHeaderField extends ObjectField { @serializable(primitive()) desc: boolean | undefined; // boolean determines sort order, undefined when no sort + // eslint-disable-next-line default-param-last constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { super(); this.heading = heading; this.color = color; - this.type = type ? type : 0; - this.width = width ? width : -1; + this.type = type ?? 0; + this.width = width ?? -1; this.desc = desc; this.collapsed = collapsed; } @@ -124,6 +135,8 @@ export class SchemaHeaderField extends ObjectField { return `SchemaHeaderField`; } } + +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function schemaHeaderField(heading: string, color: string, type: number, width: number, desc?: boolean, collapsed?: boolean) { return new SchemaHeaderField(heading, color, type, width, desc, collapsed); }); diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 8b51088b2..8fe365ac2 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,17 +1,16 @@ import { action, makeObservable, observable } from 'mobx'; import { computedFn } from 'mobx-utils'; -import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr'; -import { DocServer } from '../client/DocServer'; -import { CompiledScript, CompileScript, ScriptOptions, Transformer } from '../client/util/Scripting'; -import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; -import { autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { PropSchema, SKIP, createSimpleSchema, custom, map, object, primitive, serializable } from 'serializr'; import { numberRange } from '../Utils'; -import { Doc, Field, FieldResult, Opt } from './Doc'; +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 { Copy, FieldChanged, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; -import { ObjectField } from './ObjectField'; +import { ObjGetRefField, ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; -import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT'; function optional(propSchema: PropSchema) { return custom( @@ -44,7 +43,9 @@ const scriptSchema = createSimpleSchema({ originalScript: true, }); -function finalizeScript(script: ScriptField) { +// eslint-disable-next-line no-use-before-define +function finalizeScript(scriptIn: ScriptField) { + const script = scriptIn; const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); @@ -54,11 +55,13 @@ function finalizeScript(script: ScriptField) { if (!compset.compiled) { throw new Error("Couldn't compile setter script"); } - (script as any).setterscript = compset; + script.setterscript = compset; } return comp; } -async function deserializeScript(script: ScriptField) { +// eslint-disable-next-line no-use-before-define +async function deserializeScript(scriptIn: ScriptField) { + const script = scriptIn; if (script.captures) { const captured: any = {}; (script.script.options as ScriptOptions).capturedVariables = captured; @@ -68,13 +71,16 @@ async function deserializeScript(script: ScriptField) { const val = capture.split(':')[1]; if (val === 'true') captured[key] = true; else if (val === 'false') captured[key] = false; - else if (val.startsWith('ID->')) captured[key] = await DocServer.GetRefField(val.replace('ID->', '')); + else if (val.startsWith('ID->')) captured[key] = await ObjGetRefField(val.replace('ID->', '')); else if (!isNaN(Number(val))) captured[key] = Number(val); else captured[key] = val; }) - ).then(() => ((script as any).script = finalizeScript(script))); + ).then(() => { + script.script = finalizeScript(script); + }); } else { - (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script); + // eslint-disable-next-line no-use-before-define + script.script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script); } } @@ -84,9 +90,9 @@ export class ScriptField extends ObjectField { @serializable readonly rawscript: string | undefined; @serializable(object(scriptSchema)) - readonly script: CompiledScript; + script: CompiledScript; @serializable(object(scriptSchema)) - readonly setterscript: CompiledScript | undefined; + setterscript: CompiledScript | undefined; @serializable @observable _cachedResult: FieldResult = undefined; @@ -131,11 +137,11 @@ export class ScriptField extends ObjectField { [ToString]() { return this.script.originalScript; } + // eslint-disable-next-line default-param-last public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) { return CompileScript(script, { params: { this: Doc?.name || 'Doc', // this is the doc that executes the script - self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script documentView: 'any', _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run. _setCacheResult_: 'any', // set the cached value of the function @@ -150,20 +156,23 @@ export class ScriptField extends ObjectField { }); } + // eslint-disable-next-line default-param-last public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } + // eslint-disable-next-line default-param-last public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } - public static CallGpt(queryText: string, setVal: (val: FieldResult) => void, target: Doc) { + public static CallGpt(queryTextIn: string, setVal: (val: FieldResult) => void, target: Doc) { + let queryText = queryTextIn; if (typeof queryText === 'string' && setVal) { while (queryText.match(/\(this\.[a-zA-Z_]*\)/)?.length) { const fieldRef = queryText.split('(this.')[1].replace(/\).*/, ''); - queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as Field)); + queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as FieldType)); } setVal(`Chat Pending: ${queryText}`); gptAPICall(queryText, GPTCallType.COMPLETION).then(result => { @@ -198,24 +207,29 @@ export class ComputedField extends ScriptField { } _lastComputedResult: FieldResult; - value = (doc:Doc) => (this._lastComputedResult = this._cachedResult ?? - computedFn((doc: Doc) => - this.script.compiled && - this.script.run( { - this: doc, - //value: '', - _setCacheResult_: this.setCacheResult, - _last_: this._lastComputedResult, - _readOnly_: true, - }, - console.log - ).result - )(doc) - ); // prettier-ignore + value = (doc: Doc) => { + this._lastComputedResult = + this._cachedResult ?? + computedFn(() => + this.script.compiled && + this.script.run( + { + this: doc, + // value: '', + _setCacheResult_: this.setCacheResult, + _last_: this._lastComputedResult, + _readOnly_: true, + }, + console.log + ).result + )(); // prettier-ignore + return this._lastComputedResult; + }; - [ToValue](doc: Doc) { if (ComputedField.useComputed) return { value: this.value(doc) }; } // prettier-ignore + [ToValue](doc: Doc) { return ComputedField.useComputed ? { value: this.value(doc) } : undefined; } // prettier-ignore [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore + // eslint-disable-next-line default-param-last public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) { const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables }); const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined; @@ -225,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(i => undefined) as any as number[]); + const flist = new List<number>(numberRange(curTimecode + 1).map(() => undefined) as any as number[]); flist[curTimecode] = Cast(doc[fieldKey], 'number', null); doc[`${fieldKey}_indexed`] = flist; } @@ -235,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(i => undefined) as any as string[]); + const flist = new List<string>(numberRange(curTimecode + 1).map(() => undefined) as any as string[]); flist[curTimecode] = StrCast(doc[fieldKey]); doc[`${fieldKey}_indexed`] = flist; } @@ -244,9 +258,9 @@ export class ComputedField extends ScriptField { return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { - if (doc[`${fieldKey}`] instanceof List) return; + if (doc[`${fieldKey}`] instanceof List) return undefined; if (!doc[`${fieldKey}_indexed`]) { - const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]); + const flist = new List<FieldType>(numberRange(curTimecode + 1).map(() => undefined) as any as FieldType[]); flist[curTimecode] = Field.Copy(doc[fieldKey]); doc[`${fieldKey}_indexed`] = flist; } @@ -257,11 +271,13 @@ export class ComputedField extends ScriptField { false, {} ); - return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined); + doc[fieldKey] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + return doc[fieldKey]; } } ScriptingGlobals.add( + // eslint-disable-next-line prefer-arrow-callback function setIndexVal(list: any[], index: number, value: any) { while (list.length <= index) list.push(undefined); list[index] = value; @@ -271,6 +287,7 @@ ScriptingGlobals.add( ); ScriptingGlobals.add( + // eslint-disable-next-line prefer-arrow-callback function getIndexVal(list: any[], index: number, defaultVal: Opt<number> = undefined) { return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal); }, @@ -279,12 +296,14 @@ ScriptingGlobals.add( ); ScriptingGlobals.add( + // eslint-disable-next-line prefer-arrow-callback function makeScript(script: string) { return ScriptField.MakeScript(script); }, 'returns the value at a given index of a list', '(list: any[], index: number)' ); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function dashCallChat(setVal: (val: FieldResult) => void, target: Doc, queryText: string) { ScriptField.CallGpt(queryText, setVal, target); }, 'calls chat gpt for the query string and then calls setVal with the result'); diff --git a/src/fields/Types.ts b/src/fields/Types.ts index 337e8ca21..26196d15d 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -1,5 +1,5 @@ import { DateField } from './DateField'; -import { Doc, Field, FieldResult, Opt } from './Doc'; +import { Doc, FieldType, FieldResult, Opt } from './Doc'; import { List } from './List'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -7,63 +7,69 @@ import { RichTextField } from './RichTextField'; import { ScriptField } from './ScriptField'; import { CsvField, ImageField, 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 DefaultFieldConstructor<T extends FieldType> = { + type: ToConstructor<T>; + defaultVal: T; +}; +// 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 ToType<T extends InterfaceValue> = T extends 'string' ? string : T extends 'number' - ? number - : T extends 'boolean' - ? boolean - : T extends ListSpec<infer U> - ? List<U> - : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never; - T extends DefaultFieldConstructor<infer _U> - ? never - : T extends { new (...args: any[]): List<Field> } - ? never - : T extends { new (...args: any[]): infer R } - ? R - : T extends (doc?: Doc) => infer R - ? R - : never; - -export type ToConstructor<T extends Field> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T; + ? number + : T extends 'boolean' + ? boolean + : T extends ListSpec<infer U> + ? List<U> + : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + T extends DefaultFieldConstructor<infer _U> + ? never + : T extends { new (...args: any[]): List<FieldType> } + ? never + : T extends { new (...args: any[]): infer R } + ? R + : T extends (doc?: Doc) => infer R + ? R + : never; +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 ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> }; -export type ListSpec<T extends Field> = { List: ToConstructor<T> }; - -export type DefaultFieldConstructor<T extends Field> = { - type: ToConstructor<T>; - defaultVal: T; -}; - // type 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; +// 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 InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any); -//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial -export interface Interface { - [key: string]: InterfaceValue; - // [key: string]: ToConstructor<Field> | ListSpec<Field[]>; -} -export type WithoutRefField<T extends Field> = T extends RefField ? never : T; +export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>; -export type CastCtor = ToConstructor<Field> | ListSpec<Field>; +type WithoutList<T extends FieldType> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T; export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>; +// eslint-disable-next-line no-redeclare export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; +// eslint-disable-next-line no-redeclare export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, 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; } if (field !== undefined && !(field instanceof Promise)) { if (typeof ctor === 'string') { + // eslint-disable-next-line valid-typeof if (typeof field === ctor) { return field as ToType<T>; } @@ -80,6 +86,10 @@ 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 DocCast(field: FieldResult, defaultVal?: Doc) { const doc = Cast(field, Doc, null); return doc && !(doc instanceof Promise) ? doc : (defaultVal as Doc); @@ -116,18 +126,18 @@ export function ImageCast(field: FieldResult, defaultVal: ImageField | null = nu return Cast(field, ImageField, defaultVal); } -type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T; - -export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>; -export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>; -export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> { +export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>; +// eslint-disable-next-line no-redeclare +export function FieldValue<T extends FieldType>(field: FieldResult<T>): Opt<T>; +// eslint-disable-next-line no-redeclare +export function FieldValue<T extends FieldType>(field: FieldResult<T>, defaultValue?: T): Opt<T> { return field instanceof Promise || field === undefined ? defaultValue : field; } export interface PromiseLike<T> { then(callback: (field: Opt<T>) => void): void; } -export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> { +export function PromiseValue<T extends FieldType>(field: FieldResult<T>): PromiseLike<Opt<T>> { if (field instanceof Promise) return field as Promise<Opt<T>>; return { then(cb: (field: Opt<T>) => void) { diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 87334ad16..3a83e7ca0 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -1,18 +1,14 @@ +import { custom, serializable } from 'serializr'; +import { ClientUtils } from '../ClientUtils'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { serializable, custom } from 'serializr'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; -import { ToScriptString, ToString, Copy, ToJavascriptString } from './FieldSymbols'; -import { scriptingGlobal } from '../client/util/ScriptingGlobals'; -import { Utils } from '../Utils'; function url() { return custom( - function (value: URL) { - return value?.origin === window.location.origin ? value.pathname : value?.href; - }, - function (jsonValue: string) { - return new URL(jsonValue, window.location.origin); - } + (value: URL) => (value?.origin === window.location.origin ? value.pathname : value?.href), + (jsonValue: string) => new URL(jsonValue, window.location.origin) ); } @@ -20,30 +16,34 @@ export abstract class URLField extends ObjectField { @serializable(url()) readonly url: URL; - constructor(url: string); - constructor(url: URL); - constructor(url: URL | string) { + constructor(urlVal: string); + // eslint-disable-next-line @typescript-eslint/no-shadow + constructor(urlVal: URL); + // eslint-disable-next-line @typescript-eslint/no-shadow + constructor(urlVal: URL | string) { super(); - if (typeof url === 'string') { - url = url.startsWith('http') ? new URL(url) : new URL(url, window.location.origin); - } - this.url = url; + this.url = + typeof urlVal !== 'string' + ? urlVal // it's an URL + : urlVal.startsWith('http') + ? new URL(urlVal) + : new URL(urlVal, window.location.origin); } [ToScriptString]() { - if (Utils.prepend(this.url?.pathname) === this.url?.href) { + if (ClientUtils.prepend(this.url?.pathname) === this.url?.href) { return `new ${this.constructor.name}("${this.url.pathname}")`; } return `new ${this.constructor.name}("${this.url?.href}")`; } [ToJavascriptString]() { - if (Utils.prepend(this.url?.pathname) === this.url?.href) { + if (ClientUtils.prepend(this.url?.pathname) === this.url?.href) { return `new ${this.constructor.name}("${this.url.pathname}")`; } return `new ${this.constructor.name}("${this.url?.href}")`; } [ToString]() { - if (Utils.prepend(this.url?.pathname) === this.url?.href) { + if (ClientUtils.prepend(this.url?.pathname) === this.url?.href) { return this.url.pathname; } return this.url?.href; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 1cacfe30c..335683270 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -86,7 +86,6 @@ export const documentSchema = createSchema({ followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, ) hideLinkButton: 'boolean', // whether the blue link counter button should be hidden layout_hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden - link_displayLine: 'boolean', // whether a link connection should be shown between link anchor endpoints. isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations layers: listSpec('string'), // which layers the document is part of _lockedPosition: 'boolean', // whether the document can be moved (dragged) @@ -104,7 +103,7 @@ export const documentSchema = createSchema({ export const collectionSchema = createSchema({ childLayoutTemplate: Doc, // layout template to use to render children of a collecion - childLayoutString: 'string', //layout string to use to render children of a collection + childLayoutString: 'string', // layout string to use to render children of a collection childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template) childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links onChildClick: ScriptField, // script to run for each child when its clicked @@ -113,4 +112,5 @@ export const collectionSchema = createSchema({ }); export type Document = makeInterface<[typeof documentSchema]>; +// eslint-disable-next-line no-redeclare export const Document = makeInterface(documentSchema); diff --git a/src/fields/util.ts b/src/fields/util.ts index ad592391e..9361430cb 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,12 +1,11 @@ -import { $mobx, action, observable, runInAction, trace, values } from 'mobx'; +import { $mobx, action, observable, runInAction, trace } from 'mobx'; import { computedFn } from 'mobx-utils'; -import { returnZero } from '../Utils'; +import { ClientUtils, returnZero } from '../ClientUtils'; import { DocServer } from '../client/DocServer'; -import { LinkManager } from '../client/util/LinkManager'; import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; -import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; +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'; @@ -16,24 +15,44 @@ import { RichTextField } from './RichTextField'; import { SchemaHeaderField } from './SchemaHeaderField'; import { ComputedField } from './ScriptField'; import { DocCast, ScriptCast, StrCast } from './Types'; -import { BaseException } from 'pdfjs-dist/types/src/shared/util'; + +/** + * These are the various levels of access a user can have to a document. + * + * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. + * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. + * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. + * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. + * None: the document is not shared with that user. + * Unset: Remove a sharing permission (eg., used ) + */ +export enum SharingPermissions { + Admin = 'Admin', + Edit = 'Edit', + Augment = 'Augment', + View = 'View', + None = 'Not-Shared', +} function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } -var tracing = false; +// eslint-disable-next-line prefer-const +let tracing = false; export function TraceMobx() { tracing && trace(); } -const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { +export const _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>(); + +const _setterImpl = action((target: any, prop: string | symbol | number, valueIn: any, receiver: any): boolean => { if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') { - target[prop] = value; + target[prop] = valueIn; return true; } - value = value?.[SelfProxy] ?? value; // convert any Doc type values to Proxy's + let value = valueIn?.[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])) { @@ -50,6 +69,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number throw new Error("Can't put the same object in multiple documents at the same time"); } value[Parent] = receiver; + // eslint-disable-next-line no-use-before-define value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value); } if (curValue instanceof ObjectField) { @@ -59,11 +79,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (typeof prop === 'string' && _propSetterCB.has(prop)) _propSetterCB.get(prop)!(target[SelfProxy], value); + // eslint-disable-next-line no-use-before-define const effectiveAcl = GetEffectiveAcl(target); const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail; + const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail(); const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField); const writeToServer = @@ -86,7 +107,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (value === undefined) (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}}); - if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); + if (prop === 'author' || prop.toString().startsWith('acl_')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } @@ -95,7 +116,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent( { - redo: () => (receiver[prop] = value), + redo: () => { + receiver[prop] = value; + }, undo: () => { const wasUpdate = receiver[UpdatingFromServer]; const wasForce = receiver[ForceServerWrite]; @@ -131,57 +154,16 @@ export function denormalizeEmail(email: string) { return email.replace(/__/g, '.'); } -/** - * Copies parent's acl fields to the child - */ -export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) { - [...Object.keys(parent), ...(Doc.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])] - .filter(key => key.startsWith('acl')) - .forEach(key => { - // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private. - // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key]; - const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl; - if (parAcl) { - const sharePermission = HierarchyMapping.get(parAcl)?.name; - sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly); - } - }); -} - -/** - * These are the various levels of access a user can have to a document. - * - * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. - * - * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. - * - * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. - * - * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. - * - * None: the document is not shared with that user. - * - * Unset: Remove a sharing permission (eg., used ) - */ -export enum SharingPermissions { - Admin = 'Admin', - Edit = 'Edit', - Augment = 'Augment', - View = 'View', - None = 'Not-Shared', -} - // return acl from cache or cache the acl and return. -const getEffectiveAclCache = computedFn(function (target: any, user?: string) { - return getEffectiveAcl(target, user); -}, true); +// eslint-disable-next-line no-use-before-define +const getEffectiveAclCache = computedFn((target: any, 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 { if (!target) return AclPrivate; - if (target[UpdatingFromServer] || Doc.CurrentUserEmail === 'guest') return AclAdmin; + 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) } @@ -191,10 +173,9 @@ export function GetPropAcl(target: any, prop: string | symbol | number) { return GetEffectiveAcl(target); } -let cachedGroups = observable([] as string[]); -const getCachedGroupByNameCache = computedFn(function (name: string) { - return cachedGroups.includes(name); -}, true); +const cachedGroups = observable([] as string[]); +const getCachedGroupByNameCache = computedFn((name: string) => cachedGroups.includes(name), true); + export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); } @@ -203,12 +184,12 @@ export function SetCachedGroups(groups: string[]) { } function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[DocAcl]; - if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; + if (targetAcls?.acl_Me === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; - const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group + const userChecked = user || ClientUtils.CurrentUserEmail(); // if the current user is the author of the document / the current user is a member of the admin group if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; - for (const [key, value] of Object.entries(targetAcls)) { + Object.entries(targetAcls).forEach(([key, value]) => { // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. const entity = denormalizeEmail(key.substring(4)); // an individual or a group @@ -217,7 +198,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { effectiveAcl = value as symbol; } } - } + }); return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; } @@ -229,16 +210,16 @@ function getEffectiveAcl(target: any, user?: string): symbol { /** * Recursively distributes the access right for a user across the children of a document and its annotations. - * @param key the key storing the access right (e.g. acl-groupname) + * @param key the key storing the access right (e.g. acl_groupname) * @param acl the access right being stored (e.g. "Can Edit") * @param target the document on which this access right is being set * @param visited list of Doc's already distributed to. * @param allowUpgrade whether permissions can be made less restrictive * @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well) */ -export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean, layoutOnly = false) { - const selfKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`; - if (!visited) visited = [] as Doc[]; +// eslint-disable-next-line default-param-last +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) { + const selfKey = `acl_${normalizeEmail(ClientUtils.CurrentUserEmail())}`; if (!target || visited.includes(target) || key === selfKey) return; visited.push(target); @@ -249,23 +230,21 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if (!layoutOnly && dataDoc && (allowUpgrade !== false || !dataDoc[key] || curVal > aclVal)) { // propagate ACLs to links, children, and annotations - LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade ? true : false)); + dataDoc[DirectLinks].forEach(link => distributeAcls(key, acl, link, visited, !!allowUpgrade)); DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { - distributeAcls(key, acl, d, visited, allowUpgrade ? true : false); - d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false); + distributeAcls(key, acl, d, visited, !!allowUpgrade); + d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade); }); DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => { - distributeAcls(key, acl, d, visited, allowUpgrade ? true : false); - d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false); + 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])) - .map(lkey => { - distributeAcls(key, acl, DocCast(target[lkey]), visited, allowUpgrade ? true : false); - }); + .forEach(lkey => distributeAcls(key, acl, DocCast(target[lkey]), visited, !!allowUpgrade)); if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; @@ -285,30 +264,46 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDocChanged && updateCachedAcls(dataDoc); } -export var _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>(); +/** + * Copies parent's acl fields to the child + */ +export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) { + [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail() !== parent.author ? ['acl_Owner'] : [])] + .filter(key => key.startsWith('acl_')) + .forEach(key => { + // if the default acl mode is private, then don't inherit the acl_guest permission, but set it to private. + // const permission: string = key === 'acl_Guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key]; + const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl_Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl; + if (parAcl) { + const sharePermission = HierarchyMapping.get(parAcl)?.name; + sharePermission && distributeAcls(key === 'acl_Owner' ? `acl_${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly); + } + }); +} + /** * sets a callback function to be called whenever a value is assigned to the specified field key. * For example, this is used to "publish" documents with titles that start with '@' * @param prop - * @param setter + * @param propSetter */ -export function SetPropSetterCb(prop: string, setter: ((target: any, value: any) => void) | undefined) { - _propSetterCB.set(prop, setter); +export function SetPropSetterCb(prop: string, propSetter: ((target: any, value: any) => 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, in_prop: string | symbol | number, value: any, receiver: any): boolean { - if (!in_prop) { +export function setter(target: any, inProp: string | symbol | number, value: any, receiver: any): boolean { + if (!inProp) { console.log('WARNING: trying to set an empty property. This should be fixed. '); return false; } - let prop = in_prop; - const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); + let prop = inProp; + 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))) return true; if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { if (!prop.startsWith('__')) prop = prop.substring(1); @@ -319,12 +314,24 @@ export function setter(target: any, in_prop: string | symbol | number, value: an } if (target.__fieldTuples[prop] instanceof ComputedField) { if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { - return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + return !!ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success; } } return _setter(target, prop, value, receiver); } +function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { + const field = target.__fieldTuples[prop]; + const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys + if (value) return value.value; + if (field === undefined && !ignoreProto && prop !== 'proto') { + const proto = getFieldImpl(target, 'proto', proxy, true); // TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters + if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { + return getFieldImpl(proto, prop, proxy, ignoreProto); + } + } + return field; +} export function getter(target: any, prop: string | symbol, proxy: any): any { // prettier-ignore switch (prop) { @@ -336,6 +343,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { case $mobx: return target.__fieldTuples[prop]; case DocLayout: return target.__LAYOUT__; case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; + // eslint-disable-next-line no-fallthrough default : if (typeof prop === 'symbol') return target[prop]; if (prop.startsWith('isMobX')) return target[prop]; @@ -343,23 +351,11 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined; } - const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined; - if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop]; - return getFieldImpl(target, layout_prop ?? prop, proxy); + const layoutProp = prop.startsWith('_') ? prop.substring(1) : undefined; + if (layoutProp && target.__LAYOUT__) return target.__LAYOUT__[layoutProp]; + return getFieldImpl(target, layoutProp ?? prop, proxy); } -function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { - const field = target.__fieldTuples[prop]; - const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys - if (value) return value.value; - if (field === undefined && !ignoreProto && prop !== 'proto') { - const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters - if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { - return getFieldImpl(proto, prop, proxy, ignoreProto); - } - } - return field; -} export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { return getFieldImpl(target, prop, target[SelfProxy], ignoreProto); } @@ -382,10 +378,10 @@ 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<Field> | Doc, prop: string | number, liveContainedField: ObjectField) { +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: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => { - const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) }); + 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)) }); // prettier-ignore const serverOp = diff?.op === '$addToSet' ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length } @@ -401,8 +397,8 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop: UndoManager.AddEvent( { redo: () => { - //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)); + // 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]); }, undo: action(() => { @@ -416,7 +412,7 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop: }); lastValue = ObjectField.MakeCopy((container as any)[prop as any]); }), - prop: 'add ' + diff.items?.length + ' items to list', + prop: 'add ' + (diff.items?.length ?? 0) + ' items to list', }, diff?.items ); @@ -447,12 +443,14 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop: }); lastValue = ObjectField.MakeCopy((container as any)[prop as any]); }, - prop: 'remove ' + diff.items?.length + ' items from list', + prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list', }, diff?.items ); } else { - const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field)); + const setFieldVal = (val: FieldType | undefined) => { + container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as FieldType); + }; UndoManager.AddEvent( { redo: () => { |