diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/DocServer.ts | 2 | ||||
| -rw-r--r-- | src/client/views/ScriptingRepl.tsx | 3 | ||||
| -rw-r--r-- | src/fields/CursorField.ts | 41 | ||||
| -rw-r--r-- | src/fields/Doc.ts | 21 | ||||
| -rw-r--r-- | src/fields/DocSymbols.ts | 2 | ||||
| -rw-r--r-- | src/fields/FieldSymbols.ts | 2 | ||||
| -rw-r--r-- | src/fields/List.ts | 459 | ||||
| -rw-r--r-- | src/fields/ObjectField.ts | 15 | ||||
| -rw-r--r-- | src/fields/SchemaHeaderField.ts | 14 | ||||
| -rw-r--r-- | src/fields/util.ts | 196 |
10 files changed, 378 insertions, 377 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 67be96d13..5b452b95b 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -198,7 +198,7 @@ export namespace DocServer { export namespace Control { let _isReadOnly = false; export function makeReadOnly() { - if (!Control.isReadOnly()) { + if (!_isReadOnly) { _isReadOnly = true; _CreateField = field => (_cache[field[Id]] = field); _UpdateField = emptyFunction; diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 5dfe10def..2bc2d5e6b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; import { CompileScript, Transformer, ts } from '../util/Scripting'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { undoable } from '../util/UndoManager'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; @@ -161,7 +162,7 @@ export class ScriptingRepl extends React.Component { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })); + const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts index a8a2859d2..46f5a8e1c 100644 --- a/src/fields/CursorField.ts +++ b/src/fields/CursorField.ts @@ -1,44 +1,43 @@ -import { ObjectField } from "./ObjectField"; -import { observable } from "mobx"; -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, createSimpleSchema, object, date } from "serializr"; -import { OnUpdate, ToScriptString, ToString, Copy } from "./FieldSymbols"; +import { ObjectField } from './ObjectField'; +import { observable } from 'mobx'; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, createSimpleSchema, object, date } from 'serializr'; +import { FieldChanged, ToScriptString, ToString, Copy } from './FieldSymbols'; export type CursorPosition = { - x: number, - y: number + x: number; + y: number; }; export type CursorMetadata = { - id: string, - identifier: string, - timestamp: number + id: string; + identifier: string; + timestamp: number; }; export type CursorData = { - metadata: CursorMetadata, - position: CursorPosition + metadata: CursorMetadata; + position: CursorPosition; }; const PositionSchema = createSimpleSchema({ x: true, - y: true + y: true, }); const MetadataSchema = createSimpleSchema({ id: true, identifier: true, - timestamp: true + timestamp: true, }); const CursorSchema = createSimpleSchema({ metadata: object(MetadataSchema), - position: object(PositionSchema) + position: object(PositionSchema), }); -@Deserializable("cursor") +@Deserializable('cursor') export default class CursorField extends ObjectField { - @serializable(object(CursorSchema)) readonly data: CursorData; @@ -50,7 +49,7 @@ export default class CursorField extends ObjectField { setPosition(position: CursorPosition) { this.data.position = position; this.data.metadata.timestamp = Date.now(); - this[OnUpdate]?.(); + this[FieldChanged]?.(); } [Copy]() { @@ -58,9 +57,9 @@ export default class CursorField extends ObjectField { } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { - return "invalid"; + return 'invalid'; } -}
\ No newline at end of file +} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8be295810..698e09915 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -37,11 +37,10 @@ import { Initializing, Self, SelfProxy, - Update, UpdatingFromServer, Width, } from './DocSymbols'; -import { Copy, HandleUpdate, Id, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols'; import { InkField, InkTool } from './InkField'; import { List, ListFieldName } from './List'; import { ObjectField } from './ObjectField'; @@ -52,7 +51,7 @@ import { listSpec } from './Schema'; import { ComputedField, ScriptField } from './ScriptField'; import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; -import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util'; +import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, containedFieldChangedHandler } from './util'; import JSZip = require('jszip'); export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -337,9 +336,10 @@ export class Doc extends RefField { for (const key in value) { const field = value[key]; field !== undefined && (this[FieldKeys][key] = true); - if (!(field instanceof ObjectField)) continue; - field[Parent] = this[Self]; - field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); + if (field instanceof ObjectField) { + field[Parent] = this[Self]; + field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + } } } @@ -360,12 +360,13 @@ export class Doc extends RefField { private [ForceServerWrite]: boolean = false; public [Initializing]: boolean = false; - private [Update] = (diff: any) => { - (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff); - }; - private [Self] = this; private [SelfProxy]: any; + public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => { + if (!this[UpdatingFromServer] || this[ForceServerWrite]) { + DocServer.UpdateField(this[Id], serverOp); + } + }; public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); public [Width] = () => NumCast(this[SelfProxy]._width); public [Height] = () => NumCast(this[SelfProxy]._height); diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts index 65decc147..eab26ed10 100644 --- a/src/fields/DocSymbols.ts +++ b/src/fields/DocSymbols.ts @@ -1,4 +1,4 @@ -export const Update = Symbol('DocUpdate'); +export const DocUpdated = Symbol('DocUpdated'); export const Self = Symbol('DocSelf'); export const SelfProxy = Symbol('DocSelfProxy'); export const FieldKeys = Symbol('DocFieldKeys'); diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index c381f14f5..0dbeb064b 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -1,6 +1,6 @@ export const HandleUpdate = Symbol('FieldHandleUpdate'); export const Id = Symbol('FieldId'); -export const OnUpdate = Symbol('FieldOnUpdate'); +export const FieldChanged = Symbol('FieldChanged'); export const Parent = Symbol('FieldParent'); export const Copy = Symbol('FieldCopy'); export const ToValue = Symbol('FieldToValue'); diff --git a/src/fields/List.ts b/src/fields/List.ts index 033fa569b..183d644d3 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -3,217 +3,15 @@ import { alias, list, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper'; -import { FieldTuples, Self, SelfProxy, Update } from './DocSymbols'; import { Field } from './Doc'; -import { Copy, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { FieldTuples, Self, SelfProxy } from './DocSymbols'; +import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; import { listSpec } from './Schema'; import { Cast } from './Types'; -import { deleteProperty, getter, setter, updateFunction } from './util'; - -const listHandlers: any = { - /// Mutator methods - copyWithin() { - throw new Error('copyWithin not supported yet'); - }, - fill(value: any, start?: number, end?: number) { - if (value instanceof RefField) { - throw new Error('fill with RefFields not supported yet'); - } - const res = this[Self].__fieldTuples.fill(value, start, end); - this[Update](); - return res; - }, - pop(): any { - const field = toRealField(this[Self].__fieldTuples.pop()); - this[Update](); - return field; - }, - push: action(function (this: any, ...items: any[]) { - items = items.map(toObjectField); - - const list = this[Self]; - const length = list.__fieldTuples.length; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist - if (item instanceof ObjectField) { - item[Parent] = list; - item[OnUpdate] = updateFunction(list, i + length, item, this); - } - } - const res = list.__fieldTuples.push(...items); - this[Update]({ op: '$addToSet', items, length: length + items.length }); - return res; - }), - reverse() { - const res = this[Self].__fieldTuples.reverse(); - this[Update](); - return res; - }, - shift() { - const res = toRealField(this[Self].__fieldTuples.shift()); - this[Update](); - return res; - }, - sort(cmpFunc: any) { - this[Self].__realFields(); // coerce retrieving entire array - const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); - this[Update](); - return res; - }, - splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { - this[Self].__realFields(); // coerce retrieving entire array - items = items.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 - if (item instanceof ObjectField) { - item[Parent] = list; - item[OnUpdate] = updateFunction(list, i + start, item, this); - } - } - let hintArray: { val: any; index: number }[] = []; - for (let i = start; i < start + deleteCount; i++) { - hintArray.push({ val: list.__fieldTuples[i], index: i }); - } - const res = list.__fieldTuples.splice(start, deleteCount, ...items); - // the hint object sends the starting index of the slice and the number - // of elements to delete. - this[Update]( - items.length === 0 && deleteCount - ? { op: '$remFromSet', items: removed, hint: { start: start, deleteCount: deleteCount }, length: list.__fieldTuples.length } - : items.length && !deleteCount && start === list.__fieldTuples.length - ? { op: '$addToSet', items, length: list.__fieldTuples.length } - : undefined - ); - return res.map(toRealField); - }), - unshift(...items: any[]) { - items = items.map(toObjectField); - const list = this[Self]; - const length = list.__fieldTuples.length; - 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 - if (item instanceof ObjectField) { - item[Parent] = list; - item[OnUpdate] = updateFunction(list, i, item, this); - } - } - const res = this[Self].__fieldTuples.unshift(...items); - this[Update](); - return res; - }, - /// Accessor methods - concat: action(function (this: any, ...items: any[]) { - this[Self].__realFields(); - return this[Self].__fieldTuples.map(toRealField).concat(...items); - }), - includes(valueToFind: any, fromIndex: number) { - if (valueToFind instanceof RefField) { - return this[Self].__realFields().includes(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.includes(valueToFind, fromIndex); - } - }, - indexOf(valueToFind: any, fromIndex: number) { - if (valueToFind instanceof RefField) { - return this[Self].__realFields().indexOf(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); - } - }, - join(separator: any) { - this[Self].__realFields(); - return this[Self].__fieldTuples.map(toRealField).join(separator); - }, - lastElement() { - return this[Self].__realFields().lastElement(); - }, - lastIndexOf(valueToFind: any, fromIndex: number) { - if (valueToFind instanceof RefField) { - return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); - } - }, - slice(begin: number, end: number) { - this[Self].__realFields(); - return this[Self].__fieldTuples.slice(begin, end).map(toRealField); - }, - - /// Iteration methods - entries() { - return this[Self].__realFields().entries(); - }, - every(callback: any, thisArg: any) { - return this[Self].__realFields().every(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - filter(callback: any, thisArg: any) { - return this[Self].__realFields().filter(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - find(callback: any, thisArg: any) { - return this[Self].__realFields().find(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - findIndex(callback: any, thisArg: any) { - return this[Self].__realFields().findIndex(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - forEach(callback: any, thisArg: any) { - return this[Self].__realFields().forEach(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - map(callback: any, thisArg: any) { - return this[Self].__realFields().map(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - reduce(callback: any, initialValue: any) { - return this[Self].__realFields().reduce(callback, initialValue); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); - }, - reduceRight(callback: any, initialValue: any) { - return this[Self].__realFields().reduceRight(callback, initialValue); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); - }, - some(callback: any, thisArg: any) { - return this[Self].__realFields().some(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - values() { - return this[Self].__realFields().values(); - }, - [Symbol.iterator]() { - return this[Self].__realFields().values(); - }, -}; +import { deleteProperty, getter, setter, containedFieldChangedHandler } from './util'; function toObjectField(field: Field) { return field instanceof RefField ? new ProxyField(field) : field; @@ -223,38 +21,221 @@ function toRealField(field: Field) { return field instanceof ProxyField ? field.value : field; } -function listGetter(target: any, prop: string | symbol, receiver: any): any { - if (listHandlers.hasOwnProperty(prop)) { - return listHandlers[prop]; - } - return getter(target, prop, receiver); -} - -interface ListSpliceUpdate<T> { - type: 'splice'; - index: number; - added: T[]; - removedCount: number; -} - -interface ListIndexUpdate<T> { - type: 'update'; - index: number; - newValue: T; -} - -type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>; - type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T; export const ListFieldName = 'fields'; @Deserializable('list') class ListImpl<T extends Field> extends ObjectField { + static listHandlers: any = { + /// Mutator methods + copyWithin() { + throw new Error('copyWithin not supported yet'); + }, + fill(value: any, start?: number, end?: number) { + if (value instanceof RefField) { + throw new Error('fill with RefFields not supported yet'); + } + const res = this[Self].__fieldTuples.fill(value, start, end); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + pop(): any { + const field = toRealField(this[Self].__fieldTuples.pop()); + this[SelfProxy][FieldChanged]?.(); + return field; + }, + push: action(function (this: ListImpl<any>, ...items: any[]) { + items = items.map(toObjectField); + + const list = this[Self]; + const length = list.__fieldTuples.length; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + //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); + } + } + const res = list.__fieldTuples.push(...items); + this[SelfProxy][FieldChanged]?.({ op: '$addToSet', items, length: length + items.length }); + return res; + }), + reverse() { + const res = this[Self].__fieldTuples.reverse(); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + shift() { + const res = toRealField(this[Self].__fieldTuples.shift()); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + sort(cmpFunc: any) { + this[Self].__realFields(); // coerce retrieving entire array + const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { + this[Self].__realFields(); // coerce retrieving entire array + items = items.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 + if (item instanceof ObjectField) { + item[Parent] = list; + item[FieldChanged] = containedFieldChangedHandler(this, i + start, item); + } + } + let hintArray: { val: any; index: number }[] = []; + for (let i = start; i < start + deleteCount; i++) { + hintArray.push({ val: list.__fieldTuples[i], index: i }); + } + const res = list.__fieldTuples.splice(start, deleteCount, ...items); + // the hint object sends the starting index of the slice and the number + // of elements to delete. + this[SelfProxy][FieldChanged]?.( + items.length === 0 && deleteCount + ? { op: '$remFromSet', items: removed, hint: { start, deleteCount }, length: list.__fieldTuples.length } + : items.length && !deleteCount && start === list.__fieldTuples.length + ? { op: '$addToSet', items, length: list.__fieldTuples.length } + : undefined + ); + return res.map(toRealField); + }), + unshift(...items: any[]) { + items = items.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 + if (item instanceof ObjectField) { + item[Parent] = list; + item[FieldChanged] = containedFieldChangedHandler(this, i, item); + } + } + const res = this[Self].__fieldTuples.unshift(...items); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + /// Accessor methods + concat: action(function (this: any, ...items: any[]) { + this[Self].__realFields(); + return this[Self].__fieldTuples.map(toRealField).concat(...items); + }), + includes(valueToFind: any, fromIndex: number) { + if (valueToFind instanceof RefField) { + return this[Self].__realFields().includes(valueToFind, fromIndex); + } else { + return this[Self].__fieldTuples.includes(valueToFind, fromIndex); + } + }, + indexOf(valueToFind: any, fromIndex: number) { + if (valueToFind instanceof RefField) { + return this[Self].__realFields().indexOf(valueToFind, fromIndex); + } + return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); + }, + join(separator: any) { + this[Self].__realFields(); + return this[Self].__fieldTuples.map(toRealField).join(separator); + }, + lastElement() { + return this[Self].__realFields().lastElement(); + }, + lastIndexOf(valueToFind: any, fromIndex: number) { + if (valueToFind instanceof RefField) { + return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); + } else { + return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); + } + }, + slice(begin: number, end: number) { + this[Self].__realFields(); + return this[Self].__fieldTuples.slice(begin, end).map(toRealField); + }, + + /// Iteration methods + entries() { + return this[Self].__realFields().entries(); + }, + every(callback: any, thisArg: any) { + return this[Self].__realFields().every(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + filter(callback: any, thisArg: any) { + return this[Self].__realFields().filter(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + find(callback: any, thisArg: any) { + return this[Self].__realFields().find(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + findIndex(callback: any, thisArg: any) { + return this[Self].__realFields().findIndex(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + forEach(callback: any, thisArg: any) { + return this[Self].__realFields().forEach(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + map(callback: any, thisArg: any) { + return this[Self].__realFields().map(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + reduce(callback: any, initialValue: any) { + return this[Self].__realFields().reduce(callback, initialValue); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + }, + reduceRight(callback: any, initialValue: any) { + return this[Self].__realFields().reduceRight(callback, initialValue); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + }, + some(callback: any, thisArg: any) { + return this[Self].__realFields().some(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + values() { + return this[Self].__realFields().values(); + }, + [Symbol.iterator]() { + return this[Self].__realFields().values(); + }, + }; + static listGetter(target: any, prop: string | symbol, receiver: any): any { + if (ListImpl.listHandlers.hasOwnProperty(prop)) { + return ListImpl.listHandlers[prop]; + } + return getter(target, prop, receiver); + } constructor(fields?: T[]) { super(); const list = new Proxy<this>(this, { set: setter, - get: listGetter, + get: ListImpl.listGetter, ownKeys: target => Object.keys(target.__fieldTuples), getOwnPropertyDescriptor: (target, prop) => { if (prop in target[FieldTuples]) { @@ -270,9 +251,9 @@ class ListImpl<T extends Field> extends ObjectField { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); - this[SelfProxy] = list; + this[SelfProxy] = list as any as List<Field>; // bcz: ugh .. don't know how to convince typesecript that list is a List if (fields) { - (list as any).push(...fields); + this[SelfProxy].push(...fields); } return list; } @@ -305,10 +286,10 @@ class ListImpl<T extends Field> extends ObjectField { private set __fieldTuples(value) { this[FieldTuples] = value; for (const key in value) { - const field = value[key]; - if (field instanceof ObjectField) { - field[Parent] = this[Self]; - field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); + const item = value[key]; + if (item instanceof ObjectField) { + item[Parent] = this[Self]; + item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item); } } } @@ -322,16 +303,8 @@ class ListImpl<T extends Field> extends ObjectField { // @serializable(alias("fields", list(autoObject()))) @observable private [FieldTuples]: StoredType<T>[] = []; - - private [Update] = (diff: any) => { - // console.log(diff); - const update = this[OnUpdate]; - // update && update(diff); - update?.(diff); - }; - private [Self] = this; - private [SelfProxy]: any; + private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references [ToScriptString]() { return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index daa8a7777..b5bc2952a 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -1,9 +1,14 @@ -import { RefField } from "./RefField"; -import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { ScriptingGlobals } from "../client/util/ScriptingGlobals"; +import { RefField } from './RefField'; +import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols'; +import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { Field } from './Doc'; export abstract class ObjectField { - public [OnUpdate]?: (diff?: any) => void; + // prettier-ignore + public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; + items: Field[] | undefined; + length: number | undefined; + hint?: any }, serverOp?: any) => void; public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; @@ -17,4 +22,4 @@ export namespace ObjectField { } } -ScriptingGlobals.add(ObjectField);
\ No newline at end of file +ScriptingGlobals.add(ObjectField); diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 4b1855cb0..6dde2e5aa 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, primitive } from 'serializr'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols'; +import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; @@ -82,32 +82,32 @@ export class SchemaHeaderField extends ObjectField { setHeading(heading: string) { this.heading = heading; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setColor(color: string) { this.color = color; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setType(type: ColumnType) { this.type = type; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setWidth(width: number) { this.width = width; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setDesc(desc: boolean | undefined) { this.desc = desc; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setCollapsed(collapsed: boolean | undefined) { this.collapsed = collapsed; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } [Copy]() { diff --git a/src/fields/util.ts b/src/fields/util.ts index 36f619120..4dcbf1fbe 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,15 +1,13 @@ import { $mobx, action, observable, runInAction, trace } from 'mobx'; import { computedFn } from 'mobx-utils'; import { DocServer } from '../client/DocServer'; -import { CollectionViewType } from '../client/documents/DocumentTypes'; import { LinkManager } from '../client/util/LinkManager'; import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; import { returnZero } from '../Utils'; -import CursorField from './CursorField'; -import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclSelfEdit, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols'; -import { Id, OnUpdate, Parent, ToValue } from './FieldSymbols'; +import { aclLevel, Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; +import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; @@ -51,11 +49,11 @@ 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; - value[OnUpdate] = updateFunction(target, prop, value, receiver); + value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value); } if (curValue instanceof ObjectField) { delete curValue[Parent]; - delete curValue[OnUpdate]; + delete curValue[FieldChanged]; } const effectiveAcl = GetEffectiveAcl(target); @@ -81,8 +79,10 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } if (writeToServer) { - if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); - else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } }); + // prettier-ignore + if (value === undefined) + (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } }); + else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}}); if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); @@ -229,7 +229,8 @@ function getEffectiveAcl(target: any, user?: string): symbol { * @param key the key storing the access right (e.g. acl-groupname) * @param acl the access right being stored (e.g. "Can Edit") * @param target the document on which this access right is being set - * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection) + * @param visited list of Doc's already distributed to. + * @param allowUpgrade whether permissions can be made less restrictive * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean) { @@ -274,6 +275,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDocChanged && updateCachedAcls(dataDoc); } +// +// 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 { let prop = in_prop; const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); @@ -344,86 +348,104 @@ export function deleteProperty(target: any, prop: string | number | symbol) { return true; } -export function updateFunction(target: any, prop: any, value: any, receiver: any) { - let lastValue = ObjectField.MakeCopy(value); - return (diff?: any) => { - const op = - diff?.op === '$addToSet' - ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } - : diff?.op === '$remFromSet' - ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)), hint: diff.hint } } - : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } }; - !op.$set && ((op as any).length = diff.length); - const prevValue = ObjectField.MakeCopy(lastValue as List<any>); - lastValue = ObjectField.MakeCopy(value); - const newValue = ObjectField.MakeCopy(value); - - if (!(value instanceof CursorField) && !value?.some?.((v: any) => v instanceof CursorField)) { - !receiver[UpdatingFromServer] && +// this function creates a function that can be used to setup Undo for whenever an ObjectField changes. +// the idea is that the Doc field setter can only setup undo at the granularity of an entire field and won't even be called if +// just a part of a field (eg. field within an ObjectField) changes. This function returns a function that can be called +// whenever an internal ObjectField field changes. It should be passed a 'diff' specification describing the change. Currently, +// List's are the only true ObjectFields that can be partially modified (ignoring SchemaHeaderFields which should go away). +// The 'diff' specification that a list can send is limited to indicating that something was added, removed, or that the list contents +// 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) { + 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)) }); + // prettier-ignore + const serverOp = diff?.op === '$addToSet' + ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length } + : diff?.op === '$remFromSet' + ? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint}, length: diff.length } + : { $set: { ['fields.' + prop]: liveContainedField ? SerializationHelper.Serialize(liveContainedField) : undefined } }; + + if (!(container instanceof Doc) || !container[UpdatingFromServer]) { + const prevValue = ObjectField.MakeCopy(lastValue as List<any>); + lastValue = ObjectField.MakeCopy(liveContainedField); + const newValue = ObjectField.MakeCopy(liveContainedField); + if (diff?.op === '$addToSet') { UndoManager.AddEvent( - diff?.op === '$addToSet' - ? { - redo: () => { - console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo - receiver[prop].push(...diff.items.map((item: any) => item.value ?? item)); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: action(() => { - // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - if (item instanceof SchemaHeaderField) { - const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); - ind !== -1 && receiver[prop].splice(ind, 1); - } else { - const ind = receiver[prop].indexOf(item.value ?? item); - ind !== -1 && receiver[prop].splice(ind, 1); - } - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - prop: 'add ' + diff.items.length + ' items to list', - } - : diff?.op === '$remFromSet' - ? { - redo: action(() => { - console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item); - ind !== -1 && receiver[prop].splice(ind, 1); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - undo: () => { - // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - if (item instanceof SchemaHeaderField) { - const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); - ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item); - } else { - const ind = (prevValue as List<any>).indexOf(item.value ?? item); - ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item); - } - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: 'remove ' + diff.items.length + ' items from list', - } - : { - redo: () => { - console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo - receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: () => { - // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo - receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: 'assign list', - }, + { + 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)); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + undo: action(() => { + // console.log('undo $add: ' + prop, diff.items); // bcz: uncomment to log undo + diff.items?.forEach((item: any) => { + const ind = + item instanceof SchemaHeaderField // + ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) + : (container as any)[prop as any]?.indexOf(item.value ?? item); + ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }), + prop: 'add ' + diff.items?.length + ' items to list', + }, + diff?.items + ); + } else if (diff?.op === '$remFromSet') { + UndoManager.AddEvent( + { + redo: action(() => { + // console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo + diff.items?.forEach((item: any) => { + const ind = + item instanceof SchemaHeaderField // + ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) + : (container as any)[prop as any]?.indexOf(item.value ?? item); + ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }), + undo: () => { + // console.log('undo $rem: ' + prop, diff.items); // bcz: uncomment to log undo + diff.items?.forEach((item: any) => { + if (item instanceof SchemaHeaderField) { + const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && (container as any)[prop as any].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && (container as any)[prop as any].splice(ind, 0, item); + } else { + const ind = (prevValue as List<any>).indexOf(item.value ?? item); + ind !== -1 && (container as any)[prop as any].indexOf(item.value ?? item) === -1 && (container as any)[prop as any].splice(ind, 0, item); + } + }); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + prop: 'remove ' + diff.items?.length + ' 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)); + UndoManager.AddEvent( + { + redo: () => { + // console.log('redo list: ' + prop, fieldVal()); // bcz: uncomment to log undo + setFieldVal(newValue instanceof ObjectField ? ObjectField.MakeCopy(newValue) : undefined); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + undo: () => { + // console.log('undo list: ' + prop, fieldVal()); // bcz: uncomment to log undo + setFieldVal(prevValue instanceof ObjectField ? ObjectField.MakeCopy(prevValue) : undefined); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + prop: 'set list field', + }, + diff?.items + ); + } } - target[Update](op); + container[FieldChanged]?.(undefined, serverOp); }; } |
