diff options
Diffstat (limited to 'src/fields')
| -rw-r--r-- | src/fields/DateField.ts | 3 | ||||
| -rw-r--r-- | src/fields/Doc.ts | 182 | ||||
| -rw-r--r-- | src/fields/InkField.ts | 11 | ||||
| -rw-r--r-- | src/fields/List.ts | 61 | ||||
| -rw-r--r-- | src/fields/ObjectField.ts | 25 | ||||
| -rw-r--r-- | src/fields/RefField.ts | 4 | ||||
| -rw-r--r-- | src/fields/RichTextUtils.ts | 17 | ||||
| -rw-r--r-- | src/fields/Schema.ts | 6 | ||||
| -rw-r--r-- | src/fields/SchemaHeaderField.ts | 25 | ||||
| -rw-r--r-- | src/fields/ScriptField.ts | 96 | ||||
| -rw-r--r-- | src/fields/Types.ts | 73 | ||||
| -rw-r--r-- | src/fields/URLField.ts | 32 | ||||
| -rw-r--r-- | src/fields/util.ts | 204 |
13 files changed, 336 insertions, 403 deletions
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index 1e5222fb6..56a3177f8 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -1,5 +1,5 @@ -import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, date } from 'serializr'; +import { Deserializable } from '../client/util/SerializationHelper'; import { ObjectField } from './ObjectField'; import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; @@ -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..60c6402d4 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,26 +1,20 @@ -import { saveAs } from 'file-saver'; 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 { 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'; @@ -28,10 +22,8 @@ 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 { 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 { /** @@ -45,7 +37,7 @@ export namespace 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 valFunc = (field: FieldType): string => { const res = field instanceof ComputedField && showComputedValue ? field.value(doc) @@ -63,7 +55,7 @@ export namespace Field { }; return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (onDelegate ? '=' : '') + valFunc(field); } - 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 +64,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,7 +74,7 @@ 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 => { @@ -95,23 +87,23 @@ 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); } export function Copy(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; } } -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. @@ -159,7 +151,7 @@ export function updateCachedAcls(doc: Doc) { if (!doc) return; const target = (doc as any)?.__fieldTuples ?? doc; - const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; + const permissions: { [key: string]: symbol } = !target.author || target.author === ClientUtils.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)); @@ -178,7 +170,8 @@ 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 = ''; + + public static AddLink: undefined | ((link: Doc, checkExists?: boolean) => void); public static get MySharedDocs() { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore @@ -320,7 +313,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 +322,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); } @@ -363,7 +356,7 @@ export class Doc extends RefField { public async [HandleUpdate](diff: any) { const set = diff.$set; - const sameAuthor = this.author === Doc.CurrentUserEmail; + const sameAuthor = this.author === ClientUtils.CurrentUserEmail; if (set) { for (const key in set) { const fprefix = 'fields.'; @@ -454,7 +447,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,7 +475,7 @@ 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) { + export async function SetInPlace(doc: Doc, key: string, value: FieldType | undefined, defaultProto: boolean) { if (key.startsWith('_')) key = key.substring(1); const hasProto = doc[DocData] !== doc ? doc[DocData] : undefined; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; @@ -500,7 +493,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,7 +504,7 @@ 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)) { @@ -634,7 +626,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 +634,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(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } @@ -700,7 +692,7 @@ export namespace Doc { }; 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 +701,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 { @@ -771,7 +763,7 @@ export namespace Doc { const copy = 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)); + linkedDocs.map(link => Doc.AddLink?.(link, true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); @@ -790,84 +782,6 @@ export namespace Doc { 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(); - } - }); - }); - } - const _pendingMap = new Set<string>(); // // Returns an expanded template layout for a target data document if there is a template relationship @@ -954,7 +868,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') { @@ -1000,7 +914,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) { @@ -1038,7 +952,7 @@ 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])); @@ -1067,7 +981,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++ + ')'); @@ -1123,7 +1037,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 { @@ -1426,20 +1340,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)) @@ -1505,7 +1405,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); @@ -1518,7 +1418,7 @@ export namespace Doc { 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; } } @@ -1617,7 +1517,7 @@ export namespace Doc { if (hasEntries || !excludeEmptyObjects) { const resolved = target ?? new Doc(); if (hasEntries) { - let result: Opt<Field>; + let result: Opt<FieldType>; Object.keys(object).map(key => { // if excludeEmptyObjects is true, any qualifying conversions from toField will // be undefined, and thus the results that would have @@ -1639,9 +1539,9 @@ 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 @@ -1651,7 +1551,7 @@ export namespace Doc { } }; - 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; } @@ -1719,7 +1619,7 @@ ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, ran Doc.setDocRangeFilter(container, key, range); }); ScriptingGlobals.add(function toJavascriptString(str: string) { - return Field.toJavascriptString(str as Field); + return Field.toJavascriptString(str as FieldType); }); ScriptingGlobals.add(function RtfField() { return RichTextField.RTFfield(); 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..f6e0473ea 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -1,31 +1,30 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { alias, list, serializable } from 'serializr'; -import { DocServer } from '../client/DocServer'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper'; -import { Field } from './Doc'; +import { Field, FieldType } 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 +43,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 +76,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 +106,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 +130,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 +149,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; @@ -244,7 +241,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 +252,12 @@ 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 + 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 +269,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])))); @@ -308,7 +306,7 @@ 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 + 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))}]`; @@ -320,10 +318,11 @@ class ListImpl<T extends Field> extends ObjectField { return `[${(this as any).map((field: any) => Field.toString(field))}]`; } } -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; +export type List<T extends FieldType> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; +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'), []); diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index e1b5b036c..6c70adc1d 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -1,26 +1,35 @@ -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; + 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/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..d75d66bf8 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -1,21 +1,22 @@ import { AssertionError } from 'assert'; +import * as Color from 'color'; import { docs_v1 } 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 { DocUtils, Docs } from '../client/documents/Documents'; 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'; @@ -140,8 +141,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), }); } } @@ -401,7 +402,7 @@ export namespace RichTextUtils { DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument); linkDoc.link_anchor_2 = exported; } - url = Utils.shareUrl(exported[Id]); + url = ClientUtils.shareUrl(exported[Id]); } } value = { url }; diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index f5e64ae1f..364899dc7 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -1,5 +1,5 @@ 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'; @@ -99,11 +99,11 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon return schema as any; } -export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> { +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..8a3787768 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,6 +137,7 @@ 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: { @@ -150,20 +157,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 +208,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 +240,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 +250,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 +259,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 +272,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 +288,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 +297,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..6ed94d341 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,54 +7,55 @@ import { RichTextField } from './RichTextField'; import { ScriptField } from './ScriptField'; import { CsvField, ImageField, WebField } from './URLField'; +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; + 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 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; - -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 +// 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 WithoutRefField<T extends FieldType> = T extends RefField ? never : T; -export type CastCtor = ToConstructor<Field> | ListSpec<Field>; +export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>; + +type WithoutList<T extends FieldType> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T; export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>; export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; @@ -116,18 +117,16 @@ 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>; +export function FieldValue<T extends FieldType>(field: FieldResult<T>): Opt<T>; +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..c6c51957d 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) ); } @@ -24,26 +20,28 @@ export abstract class URLField extends ObjectField { constructor(url: URL); constructor(url: 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 url !== 'string' + ? url // it's an URL + : url.startsWith('http') + ? new URL(url) + : new URL(url, 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/util.ts b/src/fields/util.ts index ad592391e..f87c200c1 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 = @@ -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); } @@ -205,10 +186,10 @@ function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[DocAcl]; 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; } @@ -236,9 +217,9 @@ function getEffectiveAcl(target: any, user?: string): symbol { * @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,7 +264,23 @@ 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 '@' @@ -299,13 +294,13 @@ export function SetPropSetterCb(prop: string, setter: ((target: any, value: any) // // 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; @@ -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: () => { |
