aboutsummaryrefslogtreecommitdiff
path: root/src/fields/Doc.ts
diff options
context:
space:
mode:
authorgeireann <geireann.lindfield@gmail.com>2023-03-16 11:16:19 -0400
committergeireann <geireann.lindfield@gmail.com>2023-03-16 11:16:19 -0400
commita0ae93e3b14069c0de419fc5dcade84d460a0b30 (patch)
tree40a3b49fc48975be3797bcb07c96771e32bdc77b /src/fields/Doc.ts
parentab60751c36cb8cf8f87bbb9e1fe227deb3701121 (diff)
parent0e55893d0f7f2a0aa5098df73d0ece5a7f1a4ddf (diff)
Merge branch 'master' into pres-trail-sophie
Diffstat (limited to 'src/fields/Doc.ts')
-rw-r--r--src/fields/Doc.ts152
1 files changed, 72 insertions, 80 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ed5eaa756..168e29dd5 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -10,6 +10,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
import { SelectionManager } from '../client/util/SelectionManager';
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
+import { decycle } from '../decycler/decycler';
import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils';
import { DateField } from './DateField';
import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
@@ -25,7 +26,6 @@ import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Ty
import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField';
import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util';
import JSZip = require('jszip');
-
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
const onDelegate = Object.keys(doc).includes(key);
@@ -33,20 +33,19 @@ export namespace Field {
return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
}
export function toScriptString(field: Field): string {
- if (typeof field === '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}"`;
+ 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}"`;
+ case 'number':
+ case 'boolean':
+ return String(field);
}
- if (typeof field === 'number' || typeof field === 'boolean') return String(field);
- if (field === undefined || field === null) return 'null';
- return field[ToScriptString]();
+ return field?.[ToScriptString]?.() ?? 'null';
}
export function toString(field: Field): string {
- if (typeof field === 'string') return field;
- if (typeof field === 'number' || typeof field === 'boolean') return String(field);
- if (field instanceof ObjectField) return field[ToString]();
- if (field instanceof RefField) return field[ToString]();
- return '';
+ 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;
@@ -79,17 +78,14 @@ export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> {
return Cast(field, Doc);
}
-export function NumListCast(field: FieldResult) {
- return Cast(field, listSpec('number'), []);
-}
-export function StrListCast(field: FieldResult) {
- return Cast(field, listSpec('string'), []);
+export function NumListCast(field: FieldResult, defaultVal: number[] = []) {
+ return Cast(field, listSpec('number'), defaultVal);
}
-export function DocListCast(field: FieldResult) {
- return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
+export function StrListCast(field: FieldResult, defaultVal: string[] = []) {
+ return Cast(field, listSpec('string'), defaultVal);
}
-export function DocListCastOrNull(field: FieldResult) {
- return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined;
+export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) {
+ return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[];
}
export const WidthSym = Symbol('Width');
@@ -153,17 +149,8 @@ export function updateCachedAcls(doc: Doc) {
}
@scriptingGlobal
-@Deserializable('Doc', updateCachedAcls).withFields(['id'])
+@Deserializable('Doc', updateCachedAcls, ['id'])
export class Doc extends RefField {
- //TODO tfs: these should be temporary...
- private static mainDocId: string | undefined;
- public static get MainDocId() {
- return this.mainDocId;
- }
- public static set MainDocId(id: string | undefined) {
- this.mainDocId = id;
- }
-
@observable public static CurrentlyLoading: Doc[];
// removes from currently loading display
@action
@@ -582,21 +569,13 @@ export namespace Doc {
// compare whether documents or their protos match
export function AreProtosEqual(doc?: Doc, other?: Doc) {
- if (!doc || !other) return false;
- const r = doc === other;
- const r2 = Doc.GetProto(doc) === other;
- const r3 = Doc.GetProto(other) === doc;
- const r4 = Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined;
- return r || r2 || r3 || r4;
+ return doc && other && Doc.GetProto(doc) === Doc.GetProto(other);
}
// Gets the data document for the document. Note: this is mis-named -- it does not specifically
// return the doc's proto, but rather recursively searches through the proto inheritance chain
// and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
export function GetProto(doc: Doc): Doc {
- if (doc instanceof Promise) {
- // console.log("GetProto: warning: got Promise insead of Doc");
- }
const proto = doc && (Doc.GetT(doc, 'isPrototype', 'boolean', true) ? doc : doc.proto || doc);
return proto === doc ? proto : Doc.GetProto(proto);
}
@@ -722,22 +701,12 @@ export namespace Doc {
return bestAlias ?? Doc.MakeAlias(doc);
}
- export async function makeClone(
- doc: Doc,
- cloneMap: Map<string, Doc>,
- linkMap: Map<Doc, Doc>,
- rtfs: { copy: Doc; key: string; field: RichTextField }[],
- exclusions: string[],
- topLevelExclusions: string[],
- dontCreate: boolean,
- asBranch: boolean
- ): Promise<Doc> {
+ export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
if (Doc.IsBaseProto(doc)) return doc;
if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
- const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) || doc : doc) : new Doc(undefined, true);
+ const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) ?? doc : doc) : new Doc(undefined, true);
cloneMap.set(doc[Id], copy);
- const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== 'annotationOn') : exclusions;
- const filter = [...fieldExclusions, ...topLevelExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
+ const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
await Promise.all(
Object.keys(doc).map(async key => {
if (filter.includes(key)) return;
@@ -748,10 +717,10 @@ export namespace Doc {
const list = await Cast(doc[key], listSpec(Doc));
const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
if (docs !== undefined && docs.length) {
- const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch)));
+ const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
!dontCreate && assignKey(new List<Doc>(clones));
} else if (doc[key] instanceof Doc) {
- assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
+ assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
} else {
!dontCreate && assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
@@ -761,13 +730,12 @@ export namespace Doc {
}
}
};
- if (key === 'proto') {
- if (doc[key] instanceof Doc) {
- assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch));
- }
- } else if (key === 'anchor1' || key === 'anchor2') {
- if (doc[key] instanceof Doc) {
- assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], true, asBranch));
+ const docAtKey = doc[key];
+ if (docAtKey instanceof Doc) {
+ if (!Doc.IsSystem(docAtKey) && (key === 'annotationOn' || (key === 'proto' && cloneMap.has(doc[Id])) || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
+ assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
+ } else {
+ assignKey(docAtKey);
}
} else {
if (field instanceof RefField) {
@@ -786,8 +754,8 @@ export namespace Doc {
})
);
for (const link of Array.from(doc[DirectLinksSym])) {
- const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch);
- linkMap.set(link, linkClone);
+ const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
+ linkMap.set(link[Id], linkClone);
}
if (!dontCreate) {
Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true);
@@ -800,11 +768,29 @@ export namespace Doc {
Doc.AddFileOrphan(copy);
return copy;
}
+ export function repairClone(doc: Doc, cloned: Doc[], visited: Set<Doc>) {
+ if (visited.has(doc)) return;
+ visited.add(doc);
+ Object.keys(doc).map(key => {
+ const docAtKey = DocCast(doc[key]);
+ if (docAtKey && !Doc.IsSystem(docAtKey)) {
+ if (!cloned.includes(docAtKey)) {
+ doc[key] = undefined;
+ } else {
+ repairClone(docAtKey, cloned, visited);
+ }
+ }
+ });
+ }
export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
- const linkMap = new Map<Doc, Doc>();
+ const linkMap = new Map<string, Doc>();
const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
- const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], ['context'], dontCreate, asBranch);
- Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], dontCreate, asBranch);
+ const repaired = new Set<Doc>();
+ const linkedDocs = Array.from(linkMap.values());
+ const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
+ clonedDocs.map(clone => Doc.repairClone(clone, Array.from(cloneMap.values()), repaired));
+ linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
const mapped = cloneMap.get(id);
@@ -818,7 +804,7 @@ export namespace Doc {
const re = new RegExp(regex, 'g');
copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
});
- return { clone: copy, map: cloneMap };
+ return { clone: copy, map: cloneMap, linkMap };
}
export async function Zip(doc: Doc) {
@@ -827,9 +813,10 @@ export namespace Doc {
// a.href = url;
// a.download = `DocExport-${this.props.Document[Id]}.zip`;
// a.click();
- const { clone, map } = await Doc.MakeClone(doc, true);
+ const { clone, map, linkMap } = await Doc.MakeClone(doc, true);
+ clone.LINKS = new List<Doc>(Array.from(linkMap.values()));
function replacer(key: any, value: any) {
- if (['branchOf', 'cloneOf', 'context', 'cursors'].includes(key)) return undefined;
+ if (['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
else if (value instanceof Doc) {
if (key !== 'field' && Number.isNaN(Number(key))) {
const __fields = value[FieldsSym]();
@@ -854,7 +841,7 @@ export namespace Doc {
const docs: { [id: string]: any } = {};
Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
- const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+ const docString = JSON.stringify({ id: doc[Id], docs }, decycle(replacer));
const zip = new JSZip();
@@ -1304,6 +1291,7 @@ export namespace Doc {
}
export function linkFollowUnhighlight() {
+ clearTimeout(UnhighlightTimer);
UnhighlightWatchers.forEach(watcher => watcher());
UnhighlightWatchers.length = 0;
highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc));
@@ -1345,12 +1333,15 @@ export namespace Doc {
}
});
}
- export function UnHighlightDoc(doc: Doc) {
+ /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted
+ export function UnHighlightDoc(doc?: Doc) {
runInAction(() => {
- highlightedDocs.delete(doc);
- highlightedDocs.delete(Doc.GetProto(doc));
- doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
- doc[AnimationSym] = undefined;
+ (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => {
+ highlightedDocs.delete(doc);
+ highlightedDocs.delete(Doc.GetProto(doc));
+ doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
+ doc[AnimationSym] = undefined;
+ });
});
}
export function UnBrushAllDocs() {
@@ -1420,14 +1411,14 @@ export namespace Doc {
// filters document in a container collection:
// all documents with the specified value for the specified key are included/excluded
// based on the modifiers :"check", "x", undefined
- export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
+ export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) {
if (!container) return;
- const filterField = '_' + (fieldSuffix ? fieldSuffix + '-' : '') + 'docFilters';
+ const filterField = '_' + (fieldPrefix ? fieldPrefix + '-' : '') + 'docFilters';
const docFilters = Cast(container[filterField], listSpec('string'), []);
runInAction(() => {
for (let i = 0; i < docFilters.length; i++) {
const fields = docFilters[i].split(':'); // split key:value:modifier
- if (fields[0] === key && (fields[1] === value || modifiers === 'match')) {
+ if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) {
if (fields[2] === modifiers && modifiers && fields[1] === value) {
if (toggle) modifiers = 'remove';
else return;
@@ -1537,7 +1528,8 @@ export namespace Doc {
const response = await fetch(upload, { method: 'POST', body: formData });
const json = await response.json();
if (json !== 'error') {
- const doc = await DocServer.GetRefField(json);
+ const doc = DocCast(await DocServer.GetRefField(json));
+ (await DocListCastAsync(doc?.LINKS))?.forEach(link => LinkManager.Instance.addLink(link));
return doc;
}
}