diff options
Diffstat (limited to 'src/fields/Doc.ts')
-rw-r--r-- | src/fields/Doc.ts | 182 |
1 files changed, 41 insertions, 141 deletions
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(); |