From 79d8b5c812db5c28f477ace8db0ee4d9e18a84b7 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 17 Apr 2019 06:16:50 -0400 Subject: Started implementing new documents --- src/debug/Test.tsx | 39 ++++++----- src/fields/NewDoc.ts | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 src/fields/NewDoc.ts (limited to 'src') diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 11f2b0c4e..ca093e5b2 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -1,29 +1,32 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import JsxParser from 'react-jsx-parser'; +import { serialize, deserialize, map } from 'serializr'; +import { URLField, Doc } from '../fields/NewDoc'; -class Hello extends React.Component<{ firstName: string, lastName: string }> { - render() { - return
Hello {this.props.firstName} {this.props.lastName}
; +class Test extends React.Component { + onClick = () => { + const url = new URLField(new URL("http://google.com")); + const doc = new Doc("a"); + const doc2 = new Doc("b"); + doc.hello = 5; + doc.fields = "test"; + doc.test = "hello doc"; + doc.url = url; + doc.testDoc = doc2; + + console.log(doc.hello); + console.log(doc.fields); + console.log(doc.test); + console.log(doc.url); + console.log(doc.testDoc); } -} -class Test extends React.Component { render() { - let jsx = ""; - let bindings = { - props: { - firstName: "First", - lastName: "Last" - } - }; - return ; + return ; } } -ReactDOM.render(( -
- -
), +ReactDOM.render( + , document.getElementById('root') ); \ No newline at end of file diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts new file mode 100644 index 000000000..d0e518306 --- /dev/null +++ b/src/fields/NewDoc.ts @@ -0,0 +1,190 @@ +import { observable, action } from "mobx"; +import { Server } from "../client/Server"; +import { UndoManager } from "../client/util/UndoManager"; +import { serialize, deserialize, serializable, primitive } from "serializr"; + +const Type = Symbol("type"); + +const Id = Symbol("id"); +export abstract class RefField { + readonly [Id]: string; + + constructor(id: string) { + this[Id] = id; + } +} + +const Update = Symbol("Update"); +const Parent = Symbol("Parent"); +export class ObjectField { + protected [Update]?: (diff?: any) => void; + private [Parent]?: Doc; +} + +function url() { + return { + serializer: function (value: URL) { + return value.href; + }, + deserializer: function (jsonValue: string, done: (err: any, val: any) => void) { + done(undefined, new URL(jsonValue)); + } + }; +} +export class URLField extends ObjectField { + @serializable(url()) + url: URL; + + constructor(url: URL) { + super(); + this.url = url; + } +} + +export class ProxyField extends ObjectField { + constructor(); + constructor(value: T); + constructor(value?: T) { + super(); + if (value) { + this.cache = value; + this.fieldId = value[Id]; + } + } + + @serializable(primitive()) + readonly fieldId: string = ""; + + // This getter/setter and nested object thing is + // because mobx doesn't play well with observable proxies + @observable.ref + private _cache: { readonly field: T | undefined } = { field: undefined }; + private get cache(): T | undefined { + return this._cache.field; + } + private set cache(field: T | undefined) { + this._cache = { field }; + } + + private failed = false; + private promise?: Promise; + + value(callback?: ((field: T | undefined) => void)): T | undefined | null { + if (this.cache) { + callback && callback(this.cache); + return this.cache; + } + if (this.failed) { + return undefined; + } + if (!this.promise) { + // this.promise = Server.GetField(this.fieldId).then(action((field: any) => { + // this.promise = undefined; + // this.cache = field; + // if (field === undefined) this.failed = true; + // return field; + // })); + this.promise = new Promise(r => r()); + } + callback && this.promise.then(callback); + return null; + } +} + +export type Field = number | string | boolean | ObjectField | RefField; + +const Self = Symbol("Self"); +export class Doc extends RefField { + + private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { + const curValue = target.__fields[prop]; + if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { + // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically + // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way + return true; + } + if (value instanceof RefField) { + value = new ProxyField(value); + } + if (value instanceof ObjectField) { + if (value[Parent] && value[Parent] !== target) { + throw new Error("Can't put the same object in multiple documents at the same time"); + } + value[Parent] = target; + value[Update] = (diff?: any) => { + if (!diff) diff = serialize(value); + target[Update]({ [prop]: diff }); + }; + } + if (curValue instanceof ObjectField) { + delete curValue[Parent]; + delete curValue[Update]; + } + target.__fields[prop] = value; + target[Update]({ [prop]: typeof value === "object" ? serialize(value) : value }); + UndoManager.AddEvent({ + redo: () => receiver[prop] = value, + undo: () => receiver[prop] = curValue + }); + return true; + } + + private static getter(target: any, prop: string | symbol | number, receiver: any): any { + if (typeof prop !== "string") { + return undefined; + } + return Doc.getField(target, prop, receiver); + } + + private static getField(target: any, prop: string, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { + if (typeof prop === "symbol") { + return target.__fields[prop]; + } + const field = target.__fields[prop]; + if (field instanceof ProxyField) { + return field.value(callback); + } + if (field === undefined && !ignoreProto) { + const proto = Doc.getField(target, "prototype", true); + if (proto instanceof Doc) { + let field = proto[prop]; + callback && callback(field === null ? undefined : field); + return field; + } + } + callback && callback(field); + return field; + + } + + static GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise { + const self = doc[Self]; + return new Promise(res => Doc.getField(self, key, ignoreProto, res)); + } + + constructor(id: string) { + super(id); + const doc = new Proxy(this, { + set: Doc.setter, + get: Doc.getter, + deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, + defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, + }); + return doc; + } + + [key: string]: Field | null | undefined; + + @observable + private __fields: { [key: string]: Field | null | undefined } = {}; + + private [Update] = (diff?: any) => { + console.log(JSON.stringify(diff || this)); + } + + private [Self] = this; +} + +export namespace Doc { + export const Prototype = Symbol("Prototype"); +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 6afdd5e5136394c9dc739b5de390aa1b55c6360f Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 17 Apr 2019 13:51:40 -0400 Subject: Serialization is mostly working --- src/client/util/SerializationHelper.ts | 72 ++++++++++++++++++++++++++++++++++ src/debug/Test.tsx | 10 ++--- src/fields/NewDoc.ts | 45 ++++++++++++++------- 3 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 src/client/util/SerializationHelper.ts (limited to 'src') diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts new file mode 100644 index 000000000..656101c95 --- /dev/null +++ b/src/client/util/SerializationHelper.ts @@ -0,0 +1,72 @@ +import { PropSchema, serialize, deserialize, custom } from "serializr"; +import { Field } from "../../fields/NewDoc"; + +export class SerializationHelper { + + public static Serialize(obj: Field): any { + if (!obj) { + return null; + } + + if (typeof obj !== 'object') { + return obj; + } + + if (!(obj.constructor.name in reverseMap)) { + throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); + } + + const json = serialize(obj); + json.__type = reverseMap[obj.constructor.name]; + return json; + } + + public static Deserialize(obj: any): any { + if (!obj) { + return null; + } + + if (typeof obj !== 'object') { + return obj; + } + + if (!obj.__type) { + throw Error("No property 'type' found in JSON."); + } + + if (!(obj.__type in serializationTypes)) { + throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`); + } + + return deserialize(serializationTypes[obj.__type], obj); + } +} + +let serializationTypes: { [name: string]: any } = {}; +let reverseMap: { [ctor: string]: string } = {}; + +export function Deserializable(name: string): Function; +export function Deserializable(constructor: Function): void; +export function Deserializable(constructor: Function | string): Function | void { + function addToMap(name: string, ctor: Function) { + if (!(name in serializationTypes)) { + serializationTypes[name] = constructor; + reverseMap[ctor.name] = name; + } else { + throw new Error(`Name ${name} has already been registered as deserializable`); + } + } + if (typeof constructor === "string") { + return (ctor: Function) => { + addToMap(constructor, ctor); + }; + } + addToMap(constructor.name, constructor); +} + +export function autoObject(): PropSchema { + return custom( + (s) => SerializationHelper.Serialize(s), + (s) => SerializationHelper.Deserialize(s) + ); +} \ No newline at end of file diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index ca093e5b2..7e7b3a964 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { serialize, deserialize, map } from 'serializr'; import { URLField, Doc } from '../fields/NewDoc'; +import { SerializationHelper } from '../client/util/SerializationHelper'; class Test extends React.Component { onClick = () => { @@ -14,11 +15,10 @@ class Test extends React.Component { doc.url = url; doc.testDoc = doc2; - console.log(doc.hello); - console.log(doc.fields); - console.log(doc.test); - console.log(doc.url); - console.log(doc.testDoc); + console.log("doc", doc); + const cereal = Doc.Serialize(doc); + console.log("cereal", cereal); + console.log("doc again", SerializationHelper.Deserialize(cereal)); } render() { diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts index d0e518306..05dd14bc3 100644 --- a/src/fields/NewDoc.ts +++ b/src/fields/NewDoc.ts @@ -1,16 +1,15 @@ import { observable, action } from "mobx"; import { Server } from "../client/Server"; import { UndoManager } from "../client/util/UndoManager"; -import { serialize, deserialize, serializable, primitive } from "serializr"; +import { serialize, deserialize, serializable, primitive, map, alias } from "serializr"; +import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; -const Type = Symbol("type"); - -const Id = Symbol("id"); export abstract class RefField { - readonly [Id]: string; + @serializable(alias("id", primitive())) + readonly __id: string; constructor(id: string) { - this[Id] = id; + this.__id = id; } } @@ -31,6 +30,8 @@ function url() { } }; } + +@Deserializable export class URLField extends ObjectField { @serializable(url()) url: URL; @@ -41,6 +42,7 @@ export class URLField extends ObjectField { } } +@Deserializable export class ProxyField extends ObjectField { constructor(); constructor(value: T); @@ -48,7 +50,7 @@ export class ProxyField extends ObjectField { super(); if (value) { this.cache = value; - this.fieldId = value[Id]; + this.fieldId = value.__id; } } @@ -94,11 +96,17 @@ export class ProxyField extends ObjectField { export type Field = number | string | boolean | ObjectField | RefField; const Self = Symbol("Self"); + +@Deserializable export class Doc extends RefField { private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { + if (prop === "__id" || prop === "__fields") { + target[prop] = value; + return true; + } const curValue = target.__fields[prop]; - if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { + if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value.__id)) { // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way return true; @@ -130,16 +138,16 @@ export class Doc extends RefField { } private static getter(target: any, prop: string | symbol | number, receiver: any): any { - if (typeof prop !== "string") { - return undefined; + if (typeof prop === "symbol") { + return target[prop]; + } + if (prop === "__id" || prop === "__fields") { + return target[prop]; } return Doc.getField(target, prop, receiver); } - private static getField(target: any, prop: string, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { - if (typeof prop === "symbol") { - return target.__fields[prop]; - } + private static getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { const field = target.__fields[prop]; if (field instanceof ProxyField) { return field.value(callback); @@ -162,6 +170,10 @@ export class Doc extends RefField { return new Promise(res => Doc.getField(self, key, ignoreProto, res)); } + static Serialize(doc: Doc) { + return SerializationHelper.Serialize(doc[Self]); + } + constructor(id: string) { super(id); const doc = new Proxy(this, { @@ -175,6 +187,7 @@ export class Doc extends RefField { [key: string]: Field | null | undefined; + @serializable(alias("fields", map(autoObject()))) @observable private __fields: { [key: string]: Field | null | undefined } = {}; @@ -187,4 +200,6 @@ export class Doc extends RefField { export namespace Doc { export const Prototype = Symbol("Prototype"); -} \ No newline at end of file +} + +export const GetAsync = Doc.GetAsync; \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 9712a046868ee51a565a425d3216a2bb297c4eee Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 17 Apr 2019 19:45:59 -0400 Subject: Got saving to database working --- src/client/DocServer.ts | 72 +++++++++++++++++++++++++++++++++ src/client/util/SerializationHelper.ts | 73 +++++++++++++++++++++++++++++----- src/debug/Test.tsx | 6 +-- src/fields/NewDoc.ts | 52 ++++++++++++++++-------- src/server/Message.ts | 12 ++++++ src/server/database.ts | 16 ++++---- src/server/index.ts | 19 ++++++++- 7 files changed, 212 insertions(+), 38 deletions(-) create mode 100644 src/client/DocServer.ts (limited to 'src') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts new file mode 100644 index 000000000..9a3e122e8 --- /dev/null +++ b/src/client/DocServer.ts @@ -0,0 +1,72 @@ +import * as OpenSocket from 'socket.io-client'; +import { MessageStore, Types } from "./../server/Message"; +import { Opt, FieldWaiting, RefField, HandleUpdate } from '../fields/NewDoc'; +import { Utils } from '../Utils'; +import { SerializationHelper } from './util/SerializationHelper'; + +export namespace DocServer { + const _cache: { [id: string]: RefField | Promise> } = {}; + const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`); + const GUID: string = Utils.GenerateGuid(); + + export async function GetRefField(id: string): Promise> { + let cached = _cache[id]; + if (cached === undefined) { + const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => { + const field = fieldJson === undefined ? fieldJson : SerializationHelper.Deserialize(fieldJson); + if (field) { + _cache[id] = field; + } else { + delete _cache[id]; + } + return field; + }); + _cache[id] = prom; + return prom; + } else if (cached instanceof Promise) { + return cached; + } else { + return cached; + } + } + + export function UpdateField(id: string, diff: any) { + Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); + } + + export function CreateField(initialState: any) { + if (!("id" in initialState)) { + throw new Error("Can't create a field on the server without an id"); + } + Utils.Emit(_socket, MessageStore.CreateField, initialState); + } + + function respondToUpdate(diff: any) { + const id = diff.id; + if (id === undefined) { + return; + } + const field = _cache[id]; + const update = (f: Opt) => { + if (f === undefined) { + return; + } + const handler = f[HandleUpdate]; + if (handler) { + handler(diff); + } + }; + if (field instanceof Promise) { + field.then(update); + } else { + update(field); + } + } + + function connected(message: string) { + _socket.emit(MessageStore.Bar.Message, GUID); + } + + Utils.AddServerHandler(_socket, MessageStore.Foo, connected); + Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); +} \ No newline at end of file diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 656101c95..7273c3fe4 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,9 +1,13 @@ -import { PropSchema, serialize, deserialize, custom } from "serializr"; +import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr"; import { Field } from "../../fields/NewDoc"; -export class SerializationHelper { +export namespace SerializationHelper { + let serializing: number = 0; + export function IsSerializing() { + return serializing > 0; + } - public static Serialize(obj: Field): any { + export function Serialize(obj: Field): any { if (!obj) { return null; } @@ -12,16 +16,18 @@ export class SerializationHelper { return obj; } + serializing += 1; if (!(obj.constructor.name in reverseMap)) { throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); } const json = serialize(obj); json.__type = reverseMap[obj.constructor.name]; + serializing -= 1; return json; } - public static Deserialize(obj: any): any { + export function Deserialize(obj: any): any { if (!obj) { return null; } @@ -30,6 +36,7 @@ export class SerializationHelper { return obj; } + serializing += 1; if (!obj.__type) { throw Error("No property 'type' found in JSON."); } @@ -38,32 +45,78 @@ export class SerializationHelper { throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`); } - return deserialize(serializationTypes[obj.__type], obj); + const value = deserialize(serializationTypes[obj.__type], obj); + serializing -= 1; + return value; } } let serializationTypes: { [name: string]: any } = {}; let reverseMap: { [ctor: string]: string } = {}; -export function Deserializable(name: string): Function; +export interface DeserializableOpts { + (constructor: Function): void; + withFields(fields: string[]): Function; +} + +export function Deserializable(name: string): DeserializableOpts; export function Deserializable(constructor: Function): void; -export function Deserializable(constructor: Function | string): Function | void { +export function Deserializable(constructor: Function | string): DeserializableOpts | void { function addToMap(name: string, ctor: Function) { if (!(name in serializationTypes)) { - serializationTypes[name] = constructor; + serializationTypes[name] = ctor; reverseMap[ctor.name] = name; } else { throw new Error(`Name ${name} has already been registered as deserializable`); } } if (typeof constructor === "string") { - return (ctor: Function) => { + return Object.assign((ctor: Function) => { addToMap(constructor, ctor); - }; + }, { withFields: Deserializable.withFields }); } addToMap(constructor.name, constructor); } +export namespace Deserializable { + export function withFields(fields: string[]) { + return function (constructor: { new(...fields: any[]): any }) { + Deserializable(constructor); + let schema = getDefaultModelSchema(constructor); + if (schema) { + schema.factory = context => { + const args = fields.map(key => context.json[key]); + return new constructor(...args); + }; + // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing + // fields.forEach(field => { + // if (field in schema.props) { + // let propSchema = schema.props[field]; + // if (propSchema === false) { + // return; + // } else if (propSchema === true) { + // propSchema = primitive(); + // } + // schema.props[field] = custom(propSchema.serializer, + // () => { + // return SKIP; + // }); + // } + // }); + } else { + schema = { + props: {}, + factory: context => { + const args = fields.map(key => context.json[key]); + return new constructor(...args); + } + }; + setDefaultModelSchema(constructor, schema); + } + }; + } +} + export function autoObject(): PropSchema { return custom( (s) => SerializationHelper.Serialize(s), diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 7e7b3a964..660115453 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -7,8 +7,8 @@ import { SerializationHelper } from '../client/util/SerializationHelper'; class Test extends React.Component { onClick = () => { const url = new URLField(new URL("http://google.com")); - const doc = new Doc("a"); - const doc2 = new Doc("b"); + const doc = new Doc(); + const doc2 = new Doc(); doc.hello = 5; doc.fields = "test"; doc.test = "hello doc"; @@ -16,7 +16,7 @@ class Test extends React.Component { doc.testDoc = doc2; console.log("doc", doc); - const cereal = Doc.Serialize(doc); + const cereal = SerializationHelper.Serialize(doc); console.log("cereal", cereal); console.log("doc again", SerializationHelper.Deserialize(cereal)); } diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts index 05dd14bc3..46f2e20e9 100644 --- a/src/fields/NewDoc.ts +++ b/src/fields/NewDoc.ts @@ -1,16 +1,24 @@ import { observable, action } from "mobx"; import { Server } from "../client/Server"; import { UndoManager } from "../client/util/UndoManager"; -import { serialize, deserialize, serializable, primitive, map, alias } from "serializr"; +import { serializable, primitive, map, alias } from "serializr"; import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; +import { Utils } from "../Utils"; +import { DocServer } from "../client/DocServer"; +export const HandleUpdate = Symbol("HandleUpdate"); +const Id = Symbol("Id"); export abstract class RefField { @serializable(alias("id", primitive())) - readonly __id: string; + private __id: string; + readonly [Id]: string; - constructor(id: string) { - this.__id = id; + constructor(id?: string) { + this.__id = id || Utils.GenerateGuid(); + this[Id] = this.__id; } + + protected [HandleUpdate]?(diff: any): void; } const Update = Symbol("Update"); @@ -31,10 +39,10 @@ function url() { }; } -@Deserializable +@Deserializable("url") export class URLField extends ObjectField { @serializable(url()) - url: URL; + readonly url: URL; constructor(url: URL) { super(); @@ -42,7 +50,7 @@ export class URLField extends ObjectField { } } -@Deserializable +@Deserializable("proxy") export class ProxyField extends ObjectField { constructor(); constructor(value: T); @@ -50,7 +58,7 @@ export class ProxyField extends ObjectField { super(); if (value) { this.cache = value; - this.fieldId = value.__id; + this.fieldId = value[Id]; } } @@ -94,19 +102,26 @@ export class ProxyField extends ObjectField { } export type Field = number | string | boolean | ObjectField | RefField; +export type Opt = T | undefined; +export type FieldWaiting = null; +export const FieldWaiting: FieldWaiting = null; const Self = Symbol("Self"); -@Deserializable +@Deserializable("doc").withFields(["id"]) export class Doc extends RefField { private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { - if (prop === "__id" || prop === "__fields") { + if (SerializationHelper.IsSerializing()) { + target[prop] = value; + return true; + } + if (typeof prop === "symbol") { target[prop] = value; return true; } const curValue = target.__fields[prop]; - if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value.__id)) { + if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way return true; @@ -120,7 +135,7 @@ export class Doc extends RefField { } value[Parent] = target; value[Update] = (diff?: any) => { - if (!diff) diff = serialize(value); + if (!diff) diff = SerializationHelper.Serialize(value); target[Update]({ [prop]: diff }); }; } @@ -129,7 +144,7 @@ export class Doc extends RefField { delete curValue[Update]; } target.__fields[prop] = value; - target[Update]({ [prop]: typeof value === "object" ? serialize(value) : value }); + target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) }); UndoManager.AddEvent({ redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue @@ -141,7 +156,7 @@ export class Doc extends RefField { if (typeof prop === "symbol") { return target[prop]; } - if (prop === "__id" || prop === "__fields") { + if (SerializationHelper.IsSerializing()) { return target[prop]; } return Doc.getField(target, prop, receiver); @@ -174,7 +189,7 @@ export class Doc extends RefField { return SerializationHelper.Serialize(doc[Self]); } - constructor(id: string) { + constructor(id?: string, forceSave?: boolean) { super(id); const doc = new Proxy(this, { set: Doc.setter, @@ -182,6 +197,9 @@ export class Doc extends RefField { deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); + if (!id || forceSave) { + DocServer.CreateField(SerializationHelper.Serialize(doc)); + } return doc; } @@ -191,8 +209,8 @@ export class Doc extends RefField { @observable private __fields: { [key: string]: Field | null | undefined } = {}; - private [Update] = (diff?: any) => { - console.log(JSON.stringify(diff || this)); + private [Update] = (diff: any) => { + DocServer.UpdateField(this[Id], diff); } private [Self] = this; diff --git a/src/server/Message.ts b/src/server/Message.ts index bbe4ffcad..843a923d1 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -24,6 +24,14 @@ export interface Transferable { readonly data?: any; } +export interface Reference { + readonly id: string; +} + +export interface Diff extends Reference { + readonly diff: any; +} + export namespace MessageStore { export const Foo = new Message("Foo"); export const Bar = new Message("Bar"); @@ -32,4 +40,8 @@ export namespace MessageStore { export const GetFields = new Message("Get Fields"); // send string[] of 'id' get Transferable[] back export const GetDocument = new Message("Get Document"); export const DeleteAll = new Message("Delete All"); + + export const GetRefField = new Message("Get Ref Field"); + export const UpdateField = new Message("Update Ref Field"); + export const CreateField = new Message("Create Ref Field"); } diff --git a/src/server/database.ts b/src/server/database.ts index 5457e4dd5..a61b4d823 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -13,14 +13,14 @@ export class Database { this.MongoClient.connect(this.url, (err, client) => this.db = client.db()); } - public update(id: string, value: any, callback: () => void) { + public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) { if (this.db) { - let collection = this.db.collection('documents'); + let collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; let newProm: Promise; const run = (): Promise => { return new Promise(resolve => { - collection.updateOne({ _id: id }, { $set: value }, { upsert: true } + collection.updateOne({ _id: id }, { $set: value }, { upsert } , (err, res) => { if (err) { console.log(err.message); @@ -51,10 +51,12 @@ export class Database { this.db && this.db.collection(collectionName).deleteMany({}, res)); } - public insert(kvpairs: any, collectionName = Database.DocumentsCollection) { - this.db && this.db.collection(collectionName).insertOne(kvpairs, (err, res) => - err // && console.log(err) - ); + public insert(value: any, collectionName = Database.DocumentsCollection) { + if ("id" in value) { + value._id = value.id; + delete value.id; + } + this.db && this.db.collection(collectionName).insertOne(value); } public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) { diff --git a/src/server/index.ts b/src/server/index.ts index 70a7d266c..d6d5f0e55 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -22,7 +22,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo import { DashUserModel } from './authentication/models/user_model'; import { Client } from './Client'; import { Database } from './database'; -import { MessageStore, Transferable } from "./Message"; +import { MessageStore, Transferable, Diff } from "./Message"; import { RouteStore } from './RouteStore'; const app = express(); const config = require('../../webpack.config'); @@ -232,6 +232,10 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + + Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); + Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); + Utils.AddServerHandler(socket, MessageStore.GetRefField, GetRefField); }); function deleteFields() { @@ -262,5 +266,18 @@ function setField(socket: Socket, newValue: Transferable) { socket.broadcast.emit(MessageStore.SetField.Message, newValue)); } +function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { + Database.Instance.getDocument(id, callback, "newDocuments"); +} + +function UpdateField(socket: Socket, diff: Diff) { + Database.Instance.update(diff.id, diff.diff, + () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); +} + +function CreateField(newValue: any) { + Database.Instance.insert(newValue, "newDocuments"); +} + server.listen(serverPort); console.log(`listening on port ${serverPort}`); \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 8eebfed7906e1e2088d528e3af36af21094c38a9 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Thu, 18 Apr 2019 19:17:48 -0400 Subject: Implemented document schemas --- src/debug/Test.tsx | 50 ++++++++++++++++++++++++++--- src/fields/NewDoc.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 129 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 660115453..b46eb4477 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -1,9 +1,35 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { serialize, deserialize, map } from 'serializr'; -import { URLField, Doc } from '../fields/NewDoc'; +import { URLField, Doc, createSchema, makeInterface, makeStrictInterface } from '../fields/NewDoc'; import { SerializationHelper } from '../client/util/SerializationHelper'; +const schema1 = createSchema({ + hello: "number", + test: "string", + fields: "boolean", + url: URLField, + testDoc: Doc +}); + +const TestDoc = makeInterface(schema1); +type TestDoc = makeInterface; + +const schema2 = createSchema({ + hello: URLField, + test: "boolean", + fields: "string", + url: "number", + testDoc: URLField +}); + +const Test2Doc = makeStrictInterface(schema2); +type Test2Doc = makeStrictInterface; + +const assert = (bool: boolean) => { + if (!bool) throw new Error(); +}; + class Test extends React.Component { onClick = () => { const url = new URLField(new URL("http://google.com")); @@ -15,10 +41,24 @@ class Test extends React.Component { doc.url = url; doc.testDoc = doc2; - console.log("doc", doc); - const cereal = SerializationHelper.Serialize(doc); - console.log("cereal", cereal); - console.log("doc again", SerializationHelper.Deserialize(cereal)); + + const test1: TestDoc = TestDoc(doc); + const test2: Test2Doc = Test2Doc(doc); + assert(test1.hello === 5); + assert(test1.fields === undefined); + assert(test1.test === "hello doc"); + assert(test1.url === url); + assert(test1.testDoc === doc2); + test1.myField = 20; + assert(test1.myField === 20); + + assert(test2.hello === undefined); + assert(test2.fields === "test"); + assert(test2.test === undefined); + assert(test2.url === undefined); + assert(test2.testDoc === undefined); + test2.url = 35; + assert(test2.url === 35); } render() { diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts index 46f2e20e9..150b8dae8 100644 --- a/src/fields/NewDoc.ts +++ b/src/fields/NewDoc.ts @@ -1,5 +1,4 @@ import { observable, action } from "mobx"; -import { Server } from "../client/Server"; import { UndoManager } from "../client/util/UndoManager"; import { serializable, primitive, map, alias } from "serializr"; import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; @@ -185,10 +184,6 @@ export class Doc extends RefField { return new Promise(res => Doc.getField(self, key, ignoreProto, res)); } - static Serialize(doc: Doc) { - return SerializationHelper.Serialize(doc[Self]); - } - constructor(id?: string, forceSave?: boolean) { super(id); const doc = new Proxy(this, { @@ -220,4 +215,87 @@ export namespace Doc { export const Prototype = Symbol("Prototype"); } -export const GetAsync = Doc.GetAsync; \ No newline at end of file +export const GetAsync = Doc.GetAsync; + +interface IDoc { + [key: string]: Field | null | undefined; +} + +interface ImageDocument extends IDoc { + data: URLField; + test: number; +} + +export type ToType = + T extends "string" ? string : + T extends "number" ? number : + T extends "boolean" ? boolean : + T extends { new(...args: any[]): infer R } ? R : undefined; + +export type ToInterface = { + [P in keyof T]: ToType; +}; + +interface Interface { + [key: string]: { new(...args: any[]): (ObjectField | RefField) } | "number" | "boolean" | "string"; +} + +type FieldCtor = { new(): T } | "number" | "string" | "boolean"; + +function Cast(field: Field | undefined, ctor: FieldCtor): T | undefined { + if (field !== undefined) { + if (typeof ctor === "string") { + if (typeof field === ctor) { + return field as T; + } + } else if (field instanceof ctor) { + return field; + } + } + return undefined; +} + +export type makeInterface = Partial> & Doc; +export function makeInterface(schema: T): (doc: Doc) => makeInterface { + return function (doc: any) { + return new Proxy(doc, { + get(target, prop) { + const field = target[prop]; + if (prop in schema) { + return Cast(field, (schema as any)[prop]); + } + return field; + } + }); + }; +} + +export type makeStrictInterface = Partial>; +export function makeStrictInterface(schema: T): (doc: Doc) => makeStrictInterface { + const proto = {}; + for (const key in schema) { + const type = schema[key]; + Object.defineProperty(proto, key, { + get() { + return Cast(this.__doc[key], type); + }, + set(value) { + value = Cast(value, type); + if (value !== undefined) { + this.__doc[key] = value; + return; + } + throw new TypeError("Expected type " + type); + } + }); + } + return function (doc: any) { + const obj = Object.create(proto); + obj.__doc = doc; + return obj; + }; +} + +export function createSchema(schema: T): T { + return schema; +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From be5d2d30bdd98dfc32c28a84ad606eb2b4599932 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Apr 2019 02:40:46 -0400 Subject: Kind of got list typing working for schemas --- src/client/views/nodes/ImageBox.tsx | 10 ++++----- src/debug/Test.tsx | 13 ++++++++--- src/fields/NewDoc.ts | 43 +++++++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index fe0b07bc0..71b431b84 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -38,7 +38,7 @@ export class ImageBox extends React.Component { onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; - if (this._photoIndex == 0) this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); + if (this._photoIndex === 0) this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); } @@ -53,7 +53,7 @@ export class ImageBox extends React.Component { onDrop = (e: React.DragEvent) => { e.stopPropagation(); e.preventDefault(); - console.log("IMPLEMENT ME PLEASE") + console.log("IMPLEMENT ME PLEASE"); } @@ -145,9 +145,9 @@ export class ImageBox extends React.Component { let left = (nativeWidth - paths.length * dist) / 2; return paths.map((p, i) =>
-
{ e.stopPropagation(); this.onDotDown(i); }} /> +
{ e.stopPropagation(); this.onDotDown(i); }} />
- ) + ); } render() { @@ -159,7 +159,7 @@ export class ImageBox extends React.Component { let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); return (
- Image not found + Image not found {paths.length > 1 ? this.dots(paths) : (null)} {this.lightbox(paths)}
); diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index b46eb4477..033615be6 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { serialize, deserialize, map } from 'serializr'; -import { URLField, Doc, createSchema, makeInterface, makeStrictInterface } from '../fields/NewDoc'; +import { URLField, Doc, createSchema, makeInterface, makeStrictInterface, List, ListSpec } from '../fields/NewDoc'; import { SerializationHelper } from '../client/util/SerializationHelper'; const schema1 = createSchema({ @@ -18,7 +18,7 @@ type TestDoc = makeInterface; const schema2 = createSchema({ hello: URLField, test: "boolean", - fields: "string", + fields: { List: "number" } as ListSpec, url: "number", testDoc: URLField }); @@ -26,6 +26,13 @@ const schema2 = createSchema({ const Test2Doc = makeStrictInterface(schema2); type Test2Doc = makeStrictInterface; +const schema3 = createSchema({ + test: "boolean", +}); + +const Test3Doc = makeStrictInterface(schema3); +type Test3Doc = makeStrictInterface; + const assert = (bool: boolean) => { if (!bool) throw new Error(); }; @@ -53,7 +60,7 @@ class Test extends React.Component { assert(test1.myField === 20); assert(test2.hello === undefined); - assert(test2.fields === "test"); + // assert(test2.fields === "test"); assert(test2.test === undefined); assert(test2.url === undefined); assert(test2.testDoc === undefined); diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts index 150b8dae8..7be0d5146 100644 --- a/src/fields/NewDoc.ts +++ b/src/fields/NewDoc.ts @@ -107,6 +107,10 @@ export const FieldWaiting: FieldWaiting = null; const Self = Symbol("Self"); +export class List extends ObjectField { + [index: number]: T; +} + @Deserializable("doc").withFields(["id"]) export class Doc extends RefField { @@ -230,26 +234,47 @@ export type ToType = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : - T extends { new(...args: any[]): infer R } ? R : undefined; + T extends ListSpec ? List : + T extends { new(...args: any[]): infer R } ? R : never; + +export type ToConstructor = + T extends string ? "string" : + T extends number ? "number" : + T extends boolean ? "boolean" : { new(...args: any[]): T }; export type ToInterface = { [P in keyof T]: ToType; }; +// type ListSpec = { List: FieldCtor> | ListSpec> }; +export type ListSpec = { List: FieldCtor }; + +// type ListType = { 0: List>>, 1: ToType> }[HasTail extends true ? 0 : 1]; + +type Head = T extends [any, ...any[]] ? T[0] : never; +type Tail = + ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []; +type HasTail = T extends ([] | [any]) ? false : true; + interface Interface { - [key: string]: { new(...args: any[]): (ObjectField | RefField) } | "number" | "boolean" | "string"; + [key: string]: ToConstructor | ListSpec; + // [key: string]: ToConstructor | ListSpec; } -type FieldCtor = { new(): T } | "number" | "string" | "boolean"; +type FieldCtor = ToConstructor | ListSpec; -function Cast(field: Field | undefined, ctor: FieldCtor): T | undefined { +function Cast(field: Field | undefined, ctor: FieldCtor): ToType | undefined { if (field !== undefined) { if (typeof ctor === "string") { if (typeof field === ctor) { - return field as T; + return field as ToType; + } + } else if (typeof ctor === "object") { + if (field instanceof List) { + return field as ToType; } - } else if (field instanceof ctor) { - return field; + } else if (field instanceof (ctor as any)) { + return field as ToType; } } return undefined; @@ -277,10 +302,10 @@ export function makeStrictInterface(schema: T): (doc: Doc) const type = schema[key]; Object.defineProperty(proto, key, { get() { - return Cast(this.__doc[key], type); + return Cast(this.__doc[key], type as any); }, set(value) { - value = Cast(value, type); + value = Cast(value, type as any); if (value !== undefined) { this.__doc[key] = value; return; -- cgit v1.2.3-70-g09d2 From ecae4ae106be3e07471208cb93ec0965548d2d12 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Apr 2019 05:40:10 -0400 Subject: Added decent amount of list support --- src/debug/Test.tsx | 8 +++ src/fields/NewDoc.ts | 190 ++++++++++++++++++++++++++++----------------------- 2 files changed, 111 insertions(+), 87 deletions(-) (limited to 'src') diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 033615be6..6a677f80f 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -66,6 +66,14 @@ class Test extends React.Component { assert(test2.testDoc === undefined); test2.url = 35; assert(test2.url === 35); + const l = new List(); + //TODO push, and other array functions don't go through the proxy + l.push(1); + //TODO currently length, and any other string fields will get serialized + l.length = 3; + l[2] = 5; + console.log(l.slice()); + console.log(SerializationHelper.Serialize(l)); } render() { diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts index 7be0d5146..c22df4b70 100644 --- a/src/fields/NewDoc.ts +++ b/src/fields/NewDoc.ts @@ -1,6 +1,6 @@ import { observable, action } from "mobx"; import { UndoManager } from "../client/util/UndoManager"; -import { serializable, primitive, map, alias } from "serializr"; +import { serializable, primitive, map, alias, list } from "serializr"; import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; import { Utils } from "../Utils"; import { DocServer } from "../client/DocServer"; @@ -21,9 +21,10 @@ export abstract class RefField { } const Update = Symbol("Update"); +const OnUpdate = Symbol("OnUpdate"); const Parent = Symbol("Parent"); export class ObjectField { - protected [Update]?: (diff?: any) => void; + protected [OnUpdate]?: (diff?: any) => void; private [Parent]?: Doc; } @@ -107,92 +108,112 @@ export const FieldWaiting: FieldWaiting = null; const Self = Symbol("Self"); -export class List extends ObjectField { - [index: number]: T; +function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { + if (SerializationHelper.IsSerializing()) { + target[prop] = value; + return true; + } + if (typeof prop === "symbol") { + target[prop] = value; + return true; + } + const curValue = target.__fields[prop]; + if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { + // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically + // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way + return true; + } + if (value instanceof RefField) { + value = new ProxyField(value); + } + if (value instanceof ObjectField) { + if (value[Parent] && value[Parent] !== target) { + throw new Error("Can't put the same object in multiple documents at the same time"); + } + value[Parent] = target; + value[OnUpdate] = (diff?: any) => { + if (!diff) diff = SerializationHelper.Serialize(value); + target[Update]({ [prop]: diff }); + }; + } + if (curValue instanceof ObjectField) { + delete curValue[Parent]; + delete curValue[OnUpdate]; + } + target.__fields[prop] = value; + target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) }); + UndoManager.AddEvent({ + redo: () => receiver[prop] = value, + undo: () => receiver[prop] = curValue + }); + return true; } -@Deserializable("doc").withFields(["id"]) -export class Doc extends RefField { - - private static setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { - if (SerializationHelper.IsSerializing()) { - target[prop] = value; - return true; - } - if (typeof prop === "symbol") { - target[prop] = value; - return true; - } - const curValue = target.__fields[prop]; - if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { - // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically - // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way - return true; - } - if (value instanceof RefField) { - value = new ProxyField(value); - } - if (value instanceof ObjectField) { - if (value[Parent] && value[Parent] !== target) { - throw new Error("Can't put the same object in multiple documents at the same time"); - } - value[Parent] = target; - value[Update] = (diff?: any) => { - if (!diff) diff = SerializationHelper.Serialize(value); - target[Update]({ [prop]: diff }); - }; - } - if (curValue instanceof ObjectField) { - delete curValue[Parent]; - delete curValue[Update]; - } - target.__fields[prop] = value; - target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) }); - UndoManager.AddEvent({ - redo: () => receiver[prop] = value, - undo: () => receiver[prop] = curValue - }); - return true; +function getter(target: any, prop: string | symbol | number, receiver: any): any { + if (typeof prop === "symbol") { + return target.__fields[prop] || target[prop]; + } + if (SerializationHelper.IsSerializing()) { + return target[prop]; } + return getField(target, prop, receiver); +} - private static getter(target: any, prop: string | symbol | number, receiver: any): any { - if (typeof prop === "symbol") { - return target[prop]; - } - if (SerializationHelper.IsSerializing()) { - return target[prop]; +function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { + const field = target.__fields[prop]; + if (field instanceof ProxyField) { + return field.value(callback); + } + if (field === undefined && !ignoreProto) { + const proto = getField(target, "prototype", true); + if (proto instanceof Doc) { + let field = proto[prop]; + callback && callback(field === null ? undefined : field); + return field; } - return Doc.getField(target, prop, receiver); } + callback && callback(field); + return field; - private static getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { - const field = target.__fields[prop]; - if (field instanceof ProxyField) { - return field.value(callback); - } - if (field === undefined && !ignoreProto) { - const proto = Doc.getField(target, "prototype", true); - if (proto instanceof Doc) { - let field = proto[prop]; - callback && callback(field === null ? undefined : field); - return field; - } - } - callback && callback(field); - return field; +} +@Deserializable("list") +class ListImpl extends ObjectField { + constructor() { + super(); + const list = new Proxy(this, { + set: function (a, b, c, d) { return setter(a, b, c, d); }, + get: getter, + deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, + defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, + }); + return list; } - static GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise { - const self = doc[Self]; - return new Promise(res => Doc.getField(self, key, ignoreProto, res)); + [key: number]: T | null | undefined; + + @serializable(alias("fields", list(autoObject()))) + @observable + private __fields: (T | null | undefined)[] = []; + + private [Update] = (diff: any) => { + console.log(diff); + const update = this[OnUpdate]; + update && update(diff); } + private [Self] = this; +} +export type List = ListImpl & T[]; +export const List: { new (): List } = ListImpl as any; + +@Deserializable("doc").withFields(["id"]) +export class Doc extends RefField { constructor(id?: string, forceSave?: boolean) { super(id); const doc = new Proxy(this, { - set: Doc.setter, - get: Doc.getter, + set: setter, + get: getter, deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); @@ -216,20 +237,15 @@ export class Doc extends RefField { } export namespace Doc { + export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise { + const self = doc[Self]; + return new Promise(res => getField(self, key, ignoreProto, res)); + } export const Prototype = Symbol("Prototype"); } export const GetAsync = Doc.GetAsync; -interface IDoc { - [key: string]: Field | null | undefined; -} - -interface ImageDocument extends IDoc { - data: URLField; - test: number; -} - export type ToType = T extends "string" ? string : T extends "number" ? number : @@ -261,20 +277,20 @@ interface Interface { // [key: string]: ToConstructor | ListSpec; } -type FieldCtor = ToConstructor | ListSpec; +type FieldCtor = ToConstructor | ListSpec; -function Cast(field: Field | undefined, ctor: FieldCtor): ToType | undefined { +function Cast>(field: Field | undefined, ctor: T): ToType | undefined { if (field !== undefined) { if (typeof ctor === "string") { if (typeof field === ctor) { - return field as ToType; + return field as ToType; } } else if (typeof ctor === "object") { if (field instanceof List) { - return field as ToType; + return field as ToType; } } else if (field instanceof (ctor as any)) { - return field as ToType; + return field as ToType; } } return undefined; -- cgit v1.2.3-70-g09d2 From 3e96161afe4f48afa6abd1b2158b5a1c4fe85a32 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Apr 2019 07:11:13 -0400 Subject: Deleted old fields that have already been reimplemented --- src/fields/AudioField.ts | 31 ------------------ src/fields/BasicField.ts | 59 ----------------------------------- src/fields/BooleanField.ts | 25 --------------- src/fields/DocumentReference.ts | 57 ---------------------------------- src/fields/Field.ts | 69 ----------------------------------------- src/fields/FieldUpdatedArgs.ts | 27 ---------------- src/fields/ImageField.ts | 29 ----------------- src/fields/Key.ts | 50 ----------------------------- src/fields/KeyStore.ts | 65 -------------------------------------- src/fields/NewDoc.ts | 20 ++++++++++-- src/fields/NumberField.ts | 25 --------------- src/fields/PDFField.ts | 36 --------------------- src/fields/TextField.ts | 25 --------------- src/fields/VideoField.ts | 30 ------------------ src/fields/WebField.ts | 30 ------------------ 15 files changed, 18 insertions(+), 560 deletions(-) delete mode 100644 src/fields/AudioField.ts delete mode 100644 src/fields/BasicField.ts delete mode 100644 src/fields/BooleanField.ts delete mode 100644 src/fields/DocumentReference.ts delete mode 100644 src/fields/Field.ts delete mode 100644 src/fields/FieldUpdatedArgs.ts delete mode 100644 src/fields/ImageField.ts delete mode 100644 src/fields/Key.ts delete mode 100644 src/fields/KeyStore.ts delete mode 100644 src/fields/NumberField.ts delete mode 100644 src/fields/PDFField.ts delete mode 100644 src/fields/TextField.ts delete mode 100644 src/fields/VideoField.ts delete mode 100644 src/fields/WebField.ts (limited to 'src') diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts deleted file mode 100644 index 87e47a715..000000000 --- a/src/fields/AudioField.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class AudioField extends BasicField { - constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { - super(data === undefined ? new URL("http://techslides.com/demos/samples/sample.mp3") : data, save, id); - } - - toString(): string { - return this.Data.href; - } - - - ToScriptString(): string { - return `new AudioField("${this.Data}")`; - } - - Copy(): Field { - return new AudioField(this.Data); - } - - ToJson() { - return { - type: Types.Audio, - data: this.Data.href, - id: this.Id - }; - } - -} \ No newline at end of file diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts deleted file mode 100644 index 17b1fc4e8..000000000 --- a/src/fields/BasicField.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Field, FieldId } from "./Field"; -import { observable, computed, action } from "mobx"; -import { Server } from "../client/Server"; -import { UndoManager } from "../client/util/UndoManager"; - -export abstract class BasicField extends Field { - constructor(data: T, save: boolean, id?: FieldId) { - super(id); - - this.data = data; - if (save) { - Server.UpdateField(this); - } - } - - UpdateFromServer(data: any) { - if (this.data !== data) { - this.data = data; - } - } - - @observable - protected data: T; - - @computed - get Data(): T { - return this.data; - } - - set Data(value: T) { - if (this.data === value) { - return; - } - let oldValue = this.data; - this.setData(value); - UndoManager.AddEvent({ - undo: () => this.Data = oldValue, - redo: () => this.Data = value - }); - Server.UpdateField(this); - } - - protected setData(value: T) { - this.data = value; - } - - @action - TrySetValue(value: any): boolean { - if (typeof value === typeof this.data) { - this.Data = value; - return true; - } - return false; - } - - GetValue(): any { - return this.Data; - } -} diff --git a/src/fields/BooleanField.ts b/src/fields/BooleanField.ts deleted file mode 100644 index d49bfe82b..000000000 --- a/src/fields/BooleanField.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BasicField } from "./BasicField"; -import { FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class BooleanField extends BasicField { - constructor(data: boolean = false as boolean, id?: FieldId, save: boolean = true as boolean) { - super(data, save, id); - } - - ToScriptString(): string { - return `new BooleanField("${this.Data}")`; - } - - Copy() { - return new BooleanField(this.Data); - } - - ToJson() { - return { - type: Types.Boolean, - data: this.Data, - id: this.Id - }; - } -} diff --git a/src/fields/DocumentReference.ts b/src/fields/DocumentReference.ts deleted file mode 100644 index 303754177..000000000 --- a/src/fields/DocumentReference.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Field, Opt, FieldValue, FieldId } from "./Field"; -import { Document } from "./Document"; -import { Key } from "./Key"; -import { Types } from "../server/Message"; -import { ObjectID } from "bson"; - -export class DocumentReference extends Field { - get Key(): Key { - return this.key; - } - - get Document(): Document { - return this.document; - } - - constructor(private document: Document, private key: Key) { - super(); - } - - UpdateFromServer() { - - } - - Dereference(): FieldValue { - return this.document.Get(this.key); - } - - DereferenceToRoot(): FieldValue { - let field: FieldValue = this; - while (field instanceof DocumentReference) { - field = field.Dereference(); - } - return field; - } - - TrySetValue(value: any): boolean { - throw new Error("Method not implemented."); - } - GetValue() { - throw new Error("Method not implemented."); - } - Copy(): Field { - throw new Error("Method not implemented."); - } - - ToScriptString(): string { - return ""; - } - - ToJson() { - return { - type: Types.DocumentReference, - data: this.document.Id, - id: this.Id - }; - } -} \ No newline at end of file diff --git a/src/fields/Field.ts b/src/fields/Field.ts deleted file mode 100644 index 3b3e95c2b..000000000 --- a/src/fields/Field.ts +++ /dev/null @@ -1,69 +0,0 @@ - -import { Utils } from "../Utils"; -import { Types, Transferable } from "../server/Message"; -import { computed } from "mobx"; - -export function Cast(field: FieldValue, ctor: { new(): T }): Opt { - if (field) { - if (ctor && field instanceof ctor) { - return field; - } - } - return undefined; -} - -export const FieldWaiting: FIELD_WAITING = null; -export type FIELD_WAITING = null; -export type FieldId = string; -export type Opt = T | undefined; -export type FieldValue = Opt | FIELD_WAITING; - -export abstract class Field { - //FieldUpdated: TypedEvent> = new TypedEvent>(); - - init(callback: (res: Field) => any) { - callback(this); - } - - private id: FieldId; - - @computed - get Id(): FieldId { - return this.id; - } - - constructor(id: Opt = undefined) { - this.id = id || Utils.GenerateGuid(); - } - - Dereference(): FieldValue { - return this; - } - DereferenceToRoot(): FieldValue { - return this; - } - - DereferenceT(ctor: { new(): T }): FieldValue { - return Cast(this.Dereference(), ctor); - } - - DereferenceToRootT(ctor: { new(): T }): FieldValue { - return Cast(this.DereferenceToRoot(), ctor); - } - - Equals(other: Field): boolean { - return this.id === other.id; - } - - abstract UpdateFromServer(serverData: any): void; - - abstract ToScriptString(): string; - - abstract TrySetValue(value: any): boolean; - - abstract GetValue(): any; - - abstract Copy(): Field; - - abstract ToJson(): Transferable; -} \ No newline at end of file diff --git a/src/fields/FieldUpdatedArgs.ts b/src/fields/FieldUpdatedArgs.ts deleted file mode 100644 index 23ccf2a5a..000000000 --- a/src/fields/FieldUpdatedArgs.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Field, Opt } from "./Field"; -import { Document } from "./Document"; -import { Key } from "./Key"; - -export enum FieldUpdatedAction { - Add, - Remove, - Replace, - Update -} - -export interface FieldUpdatedArgs { - field: Field; - action: FieldUpdatedAction; -} - -export interface DocumentUpdatedArgs { - field: Document; - key: Key; - - oldValue: Opt; - newValue: Opt; - - fieldArgs?: FieldUpdatedArgs; - - action: FieldUpdatedAction; -} diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts deleted file mode 100644 index bce20f242..000000000 --- a/src/fields/ImageField.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class ImageField extends BasicField { - constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { - super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id); - } - - toString(): string { - return this.Data.href; - } - - ToScriptString(): string { - return `new ImageField("${this.Data}")`; - } - - Copy(): Field { - return new ImageField(this.Data); - } - - ToJson() { - return { - type: Types.Image, - data: this.Data.href, - id: this.Id - }; - } -} \ No newline at end of file diff --git a/src/fields/Key.ts b/src/fields/Key.ts deleted file mode 100644 index 57e2dadf0..000000000 --- a/src/fields/Key.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Field, FieldId } from "./Field"; -import { Utils } from "../Utils"; -import { observable } from "mobx"; -import { Types } from "../server/Message"; -import { Server } from "../client/Server"; - -export class Key extends Field { - private name: string; - - get Name(): string { - return this.name; - } - - constructor(name: string, id?: string, save: boolean = true) { - super(id || Utils.GenerateDeterministicGuid(name)); - - this.name = name; - if (save) { - Server.UpdateField(this); - } - } - - UpdateFromServer(data: string) { - this.name = data; - } - - TrySetValue(value: any): boolean { - throw new Error("Method not implemented."); - } - - GetValue() { - return this.Name; - } - - Copy(): Field { - return this; - } - - ToScriptString(): string { - return name; - } - - ToJson() { - return { - type: Types.Key, - data: this.name, - id: this.Id - }; - } -} \ No newline at end of file diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts deleted file mode 100644 index 16a909eb8..000000000 --- a/src/fields/KeyStore.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Key } from "./Key"; - -export namespace KeyStore { - export const Prototype = new Key("Prototype"); - export const X = new Key("X"); - export const Y = new Key("Y"); - export const Page = new Key("Page"); - export const Title = new Key("Title"); - export const Author = new Key("Author"); - export const PanX = new Key("PanX"); - export const PanY = new Key("PanY"); - export const Scale = new Key("Scale"); - export const NativeWidth = new Key("NativeWidth"); - export const NativeHeight = new Key("NativeHeight"); - export const Width = new Key("Width"); - export const Height = new Key("Height"); - export const ZIndex = new Key("ZIndex"); - export const Zoom = new Key("Zoom"); - export const Data = new Key("Data"); - export const Annotations = new Key("Annotations"); - export const ViewType = new Key("ViewType"); - export const Layout = new Key("Layout"); - export const BackgroundColor = new Key("BackgroundColor"); - export const BackgroundLayout = new Key("BackgroundLayout"); - export const OverlayLayout = new Key("OverlayLayout"); - export const LayoutKeys = new Key("LayoutKeys"); - export const LayoutFields = new Key("LayoutFields"); - export const ColumnsKey = new Key("SchemaColumns"); - export const SchemaSplitPercentage = new Key("SchemaSplitPercentage"); - export const Caption = new Key("Caption"); - export const ActiveWorkspace = new Key("ActiveWorkspace"); - export const DocumentText = new Key("DocumentText"); - export const BrushingDocs = new Key("BrushingDocs"); - export const LinkedToDocs = new Key("LinkedToDocs"); - export const LinkedFromDocs = new Key("LinkedFromDocs"); - export const LinkDescription = new Key("LinkDescription"); - export const LinkTags = new Key("LinkTag"); - export const Thumbnail = new Key("Thumbnail"); - export const ThumbnailPage = new Key("ThumbnailPage"); - export const CurPage = new Key("CurPage"); - export const AnnotationOn = new Key("AnnotationOn"); - export const NumPages = new Key("NumPages"); - export const Ink = new Key("Ink"); - export const Cursors = new Key("Cursors"); - export const OptionalRightCollection = new Key("OptionalRightCollection"); - export const Archives = new Key("Archives"); - export const Workspaces = new Key("Workspaces"); - export const Minimized = new Key("Minimized"); - export const CopyDraggedItems = new Key("CopyDraggedItems"); - - export const KeyList: Key[] = [Prototype, X, Y, Page, Title, Author, PanX, PanY, Scale, NativeWidth, NativeHeight, - Width, Height, ZIndex, Zoom, Data, Annotations, ViewType, Layout, BackgroundColor, BackgroundLayout, OverlayLayout, LayoutKeys, - LayoutFields, ColumnsKey, SchemaSplitPercentage, Caption, ActiveWorkspace, DocumentText, BrushingDocs, LinkedToDocs, LinkedFromDocs, - LinkDescription, LinkTags, Thumbnail, ThumbnailPage, CurPage, AnnotationOn, NumPages, Ink, Cursors, OptionalRightCollection, - Archives, Workspaces, Minimized, CopyDraggedItems - ]; - export function KeyLookup(keyid: string) { - for (const key of KeyList) { - if (key.Id === keyid) { - return key; - } - } - return undefined; - } -} diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts index c22df4b70..70dc1867c 100644 --- a/src/fields/NewDoc.ts +++ b/src/fields/NewDoc.ts @@ -241,6 +241,20 @@ export namespace Doc { const self = doc[Self]; return new Promise(res => getField(self, key, ignoreProto, res)); } + export function GetTAsync(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Promise { + const self = doc[Self]; + return new Promise(async res => { + const field = await GetAsync(doc, key, ignoreProto); + return Cast(field, ctor); + }); + } + export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined { + const self = doc[Self]; + return getField(self, key, ignoreProto); + } + export function GetT(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Field | null | undefined { + return Cast(Get(doc, key, ignoreProto), ctor); + } export const Prototype = Symbol("Prototype"); } @@ -279,8 +293,8 @@ interface Interface { type FieldCtor = ToConstructor | ListSpec; -function Cast>(field: Field | undefined, ctor: T): ToType | undefined { - if (field !== undefined) { +function Cast>(field: Field | null | undefined, ctor: T): ToType | null | undefined { + if (field !== undefined && field !== null) { if (typeof ctor === "string") { if (typeof field === ctor) { return field as ToType; @@ -292,6 +306,8 @@ function Cast>(field: Field | undefined, ctor: T): To } else if (field instanceof (ctor as any)) { return field as ToType; } + } else { + return field; } return undefined; } diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts deleted file mode 100644 index 7eea360c0..000000000 --- a/src/fields/NumberField.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Types } from "../server/Message"; -import { FieldId } from "./Field"; - -export class NumberField extends BasicField { - constructor(data: number = 0, id?: FieldId, save: boolean = true) { - super(data, save, id); - } - - ToScriptString(): string { - return `new NumberField(${this.Data})`; - } - - Copy() { - return new NumberField(this.Data); - } - - ToJson() { - return { - id: this.Id, - type: Types.Number, - data: this.Data - }; - } -} \ No newline at end of file diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts deleted file mode 100644 index 718a1a4c0..000000000 --- a/src/fields/PDFField.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { observable } from "mobx"; -import { Types } from "../server/Message"; - - - -export class PDFField extends BasicField { - constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { - super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id); - } - - toString(): string { - return this.Data.href; - } - - Copy(): Field { - return new PDFField(this.Data); - } - - ToScriptString(): string { - return `new PDFField("${this.Data}")`; - } - - ToJson() { - return { - type: Types.PDF, - data: this.Data.href, - id: this.Id - }; - } - - @observable - Page: Number = 1; - -} \ No newline at end of file diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts deleted file mode 100644 index ddedec9b1..000000000 --- a/src/fields/TextField.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BasicField } from "./BasicField"; -import { FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class TextField extends BasicField { - constructor(data: string = "", id?: FieldId, save: boolean = true) { - super(data, save, id); - } - - ToScriptString(): string { - return `new TextField("${this.Data}")`; - } - - Copy() { - return new TextField(this.Data); - } - - ToJson() { - return { - type: Types.Text, - data: this.Data, - id: this.Id - }; - } -} \ No newline at end of file diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts deleted file mode 100644 index 838b811b1..000000000 --- a/src/fields/VideoField.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class VideoField extends BasicField { - constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { - super(data === undefined ? new URL("http://techslides.com/demos/sample-videos/small.mp4") : data, save, id); - } - - toString(): string { - return this.Data.href; - } - - ToScriptString(): string { - return `new VideoField("${this.Data}")`; - } - - Copy(): Field { - return new VideoField(this.Data); - } - - ToJson() { - return { - type: Types.Video, - data: this.Data.href, - id: this.Id - }; - } - -} \ No newline at end of file diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts deleted file mode 100644 index 8b276a552..000000000 --- a/src/fields/WebField.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class WebField extends BasicField { - constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { - super(data === undefined ? new URL("https://crossorigin.me/" + "https://cs.brown.edu/") : data, save, id); - } - - toString(): string { - return this.Data.href; - } - - ToScriptString(): string { - return `new WebField("${this.Data}")`; - } - - Copy(): Field { - return new WebField(this.Data); - } - - ToJson() { - return { - type: Types.Web, - data: this.Data.href, - id: this.Id - }; - } - -} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From e678ac7f21e0c44eaa8ad88577093cdb313e21bb Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Apr 2019 23:13:17 -0400 Subject: Deleted more old fields and split new stuff into multiple files --- src/client/DocServer.ts | 2 +- src/client/util/SerializationHelper.ts | 2 +- src/debug/Test.tsx | 8 +- src/fields/HtmlField.ts | 25 --- src/fields/ListField.ts | 196 ------------------ src/fields/NewDoc.ts | 358 --------------------------------- src/fields/TupleField.ts | 59 ------ src/new_fields/Doc.ts | 90 +++++++++ src/new_fields/HtmlField.ts | 14 ++ src/new_fields/List.ts | 35 ++++ src/new_fields/Proxy.ts | 55 +++++ src/new_fields/Schema.ts | 47 +++++ src/new_fields/Types.ts | 58 ++++++ src/new_fields/URLField.ts | 25 +++ src/new_fields/util.ts | 73 +++++++ 15 files changed, 405 insertions(+), 642 deletions(-) delete mode 100644 src/fields/HtmlField.ts delete mode 100644 src/fields/ListField.ts delete mode 100644 src/fields/NewDoc.ts delete mode 100644 src/fields/TupleField.ts create mode 100644 src/new_fields/Doc.ts create mode 100644 src/new_fields/HtmlField.ts create mode 100644 src/new_fields/List.ts create mode 100644 src/new_fields/Proxy.ts create mode 100644 src/new_fields/Schema.ts create mode 100644 src/new_fields/Types.ts create mode 100644 src/new_fields/URLField.ts create mode 100644 src/new_fields/util.ts (limited to 'src') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 9a3e122e8..615e48af0 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,6 +1,6 @@ import * as OpenSocket from 'socket.io-client'; import { MessageStore, Types } from "./../server/Message"; -import { Opt, FieldWaiting, RefField, HandleUpdate } from '../fields/NewDoc'; +import { Opt, FieldWaiting, RefField, HandleUpdate } from '../new_fields/Doc'; import { Utils } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 7273c3fe4..ac70aba9d 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,5 +1,5 @@ import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr"; -import { Field } from "../../fields/NewDoc"; +import { Field } from "../../new_fields/Doc"; export namespace SerializationHelper { let serializing: number = 0; diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 6a677f80f..8b9c9fa0b 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { serialize, deserialize, map } from 'serializr'; -import { URLField, Doc, createSchema, makeInterface, makeStrictInterface, List, ListSpec } from '../fields/NewDoc'; import { SerializationHelper } from '../client/util/SerializationHelper'; +import { createSchema, makeInterface, makeStrictInterface } from '../new_fields/Schema'; +import { URLField } from '../new_fields/URLField'; +import { Doc } from '../new_fields/Doc'; +import { ListSpec } from '../new_fields/Types'; +import { List } from '../new_fields/List'; +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? const schema1 = createSchema({ hello: "number", diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts deleted file mode 100644 index a1d880070..000000000 --- a/src/fields/HtmlField.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Types } from "../server/Message"; -import { FieldId } from "./Field"; - -export class HtmlField extends BasicField { - constructor(data: string = "", id?: FieldId, save: boolean = true) { - super(data, save, id); - } - - ToScriptString(): string { - return `new HtmlField("${this.Data}")`; - } - - Copy() { - return new HtmlField(this.Data); - } - - ToJson() { - return { - type: Types.Html, - data: this.Data, - id: this.Id, - }; - } -} \ No newline at end of file diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts deleted file mode 100644 index e24099126..000000000 --- a/src/fields/ListField.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx"; -import { Server } from "../client/Server"; -import { UndoManager } from "../client/util/UndoManager"; -import { Types } from "../server/Message"; -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { FieldMap } from "../client/SocketStub"; -import { ScriptField } from "./ScriptField"; - -export class ListField extends BasicField { - private _proxies: string[] = []; - private _scriptIds: string[] = []; - private scripts: ScriptField[] = []; - - constructor(data: T[] = [], scripts: ScriptField[] = [], id?: FieldId, save: boolean = true) { - super(data, save, id); - this.scripts = scripts; - this.updateProxies(); - this._scriptIds = this.scripts.map(script => script.Id); - if (save) { - Server.UpdateField(this); - } - this.observeList(); - } - - private _processingServerUpdate: boolean = false; - - private observeDisposer: Lambda | undefined; - private observeList(): void { - if (this.observeDisposer) { - this.observeDisposer(); - } - this.observeDisposer = observe(this.Data as IObservableArray, (change: IArrayChange | IArraySplice) => { - const target = change.object; - this.updateProxies(); - if (change.type === "splice") { - this.runScripts(change.removed, false); - UndoManager.AddEvent({ - undo: () => target.splice(change.index, change.addedCount, ...change.removed), - redo: () => target.splice(change.index, change.removedCount, ...change.added) - }); - this.runScripts(change.added, true); - } else { - this.runScripts([change.oldValue], false); - UndoManager.AddEvent({ - undo: () => target[change.index] = change.oldValue, - redo: () => target[change.index] = change.newValue - }); - this.runScripts([change.newValue], true); - } - if (!this._processingServerUpdate) { - Server.UpdateField(this); - } - }); - } - - private runScripts(fields: T[], added: boolean) { - for (const script of this.scripts) { - this.runScript(fields, script, added); - } - } - - private runScript(fields: T[], script: ScriptField, added: boolean) { - if (!this._processingServerUpdate) { - for (const field of fields) { - script.script.run({ field, added }); - } - } - } - - addScript(script: ScriptField) { - this.scripts.push(script); - this._scriptIds.push(script.Id); - - this.runScript(this.Data, script, true); - UndoManager.AddEvent({ - undo: () => this.removeScript(script), - redo: () => this.addScript(script), - }); - Server.UpdateField(this); - } - - removeScript(script: ScriptField) { - const index = this.scripts.indexOf(script); - if (index === -1) { - return; - } - this.scripts.splice(index, 1); - this._scriptIds.splice(index, 1); - UndoManager.AddEvent({ - undo: () => this.addScript(script), - redo: () => this.removeScript(script), - }); - this.runScript(this.Data, script, false); - Server.UpdateField(this); - } - - protected setData(value: T[]) { - this.runScripts(this.data, false); - - this.data = observable(value); - this.updateProxies(); - this.observeList(); - this.runScripts(this.data, true); - } - - private updateProxies() { - this._proxies = this.Data.map(field => field.Id); - } - - private arraysEqual(a: any[], b: any[]) { - if (a === b) return true; - if (a === null || b === null) return false; - if (a.length !== b.length) return false; - - // If you don't care about the order of the elements inside - // the array, you should sort both arrays here. - // Please note that calling sort on an array will modify that array. - // you might want to clone your array first. - - for (var i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false; - } - return true; - } - - init(callback: (field: Field) => any) { - const fieldsPromise = Server.GetFields(this._proxies).then(action((fields: FieldMap) => { - if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) { - var dataids = this.data.map(d => d.Id); - var proxies = this._proxies.map(p => p); - var added = this.data.length < this._proxies.length; - var deleted = this.data.length > this._proxies.length; - for (let i = 0; i < dataids.length && added; i++) { - added = proxies.indexOf(dataids[i]) !== -1; - } - for (let i = 0; i < this._proxies.length && deleted; i++) { - deleted = dataids.indexOf(proxies[i]) !== -1; - } - - this._processingServerUpdate = true; - for (let i = 0; i < proxies.length && added; i++) { - if (dataids.indexOf(proxies[i]) === -1) { - this.Data.splice(i, 0, fields[proxies[i]] as T); - } - } - for (let i = dataids.length - 1; i >= 0 && deleted; i--) { - if (proxies.indexOf(dataids[i]) === -1) { - this.Data.splice(i, 1); - } - } - if (!added && !deleted) {// otherwise, just rebuild the whole list - this.setData(proxies.map(id => fields[id] as T)); - } - this._processingServerUpdate = false; - } - })); - - const scriptsPromise = Server.GetFields(this._scriptIds).then((fields: FieldMap) => { - this.scripts = this._scriptIds.map(id => fields[id] as ScriptField); - }); - - Promise.all([fieldsPromise, scriptsPromise]).then(() => callback(this)); - } - - ToScriptString(): string { - return "new ListField([" + this.Data.map(field => field.ToScriptString()).join(", ") + "])"; - } - - Copy(): Field { - return new ListField(this.Data); - } - - - UpdateFromServer(data: { fields: string[], scripts: string[] }) { - this._proxies = data.fields; - this._scriptIds = data.scripts; - } - ToJson() { - return { - type: Types.List, - data: { - fields: this._proxies, - scripts: this._scriptIds, - }, - id: this.Id - }; - } - - static FromJson(id: string, data: { fields: string[], scripts: string[] }): ListField { - let list = new ListField([], [], id, false); - list._proxies = data.fields; - list._scriptIds = data.scripts; - return list; - } -} \ No newline at end of file diff --git a/src/fields/NewDoc.ts b/src/fields/NewDoc.ts deleted file mode 100644 index 70dc1867c..000000000 --- a/src/fields/NewDoc.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { observable, action } from "mobx"; -import { UndoManager } from "../client/util/UndoManager"; -import { serializable, primitive, map, alias, list } from "serializr"; -import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; -import { Utils } from "../Utils"; -import { DocServer } from "../client/DocServer"; - -export const HandleUpdate = Symbol("HandleUpdate"); -const Id = Symbol("Id"); -export abstract class RefField { - @serializable(alias("id", primitive())) - private __id: string; - readonly [Id]: string; - - constructor(id?: string) { - this.__id = id || Utils.GenerateGuid(); - this[Id] = this.__id; - } - - protected [HandleUpdate]?(diff: any): void; -} - -const Update = Symbol("Update"); -const OnUpdate = Symbol("OnUpdate"); -const Parent = Symbol("Parent"); -export class ObjectField { - protected [OnUpdate]?: (diff?: any) => void; - private [Parent]?: Doc; -} - -function url() { - return { - serializer: function (value: URL) { - return value.href; - }, - deserializer: function (jsonValue: string, done: (err: any, val: any) => void) { - done(undefined, new URL(jsonValue)); - } - }; -} - -@Deserializable("url") -export class URLField extends ObjectField { - @serializable(url()) - readonly url: URL; - - constructor(url: URL) { - super(); - this.url = url; - } -} - -@Deserializable("proxy") -export class ProxyField extends ObjectField { - constructor(); - constructor(value: T); - constructor(value?: T) { - super(); - if (value) { - this.cache = value; - this.fieldId = value[Id]; - } - } - - @serializable(primitive()) - readonly fieldId: string = ""; - - // This getter/setter and nested object thing is - // because mobx doesn't play well with observable proxies - @observable.ref - private _cache: { readonly field: T | undefined } = { field: undefined }; - private get cache(): T | undefined { - return this._cache.field; - } - private set cache(field: T | undefined) { - this._cache = { field }; - } - - private failed = false; - private promise?: Promise; - - value(callback?: ((field: T | undefined) => void)): T | undefined | null { - if (this.cache) { - callback && callback(this.cache); - return this.cache; - } - if (this.failed) { - return undefined; - } - if (!this.promise) { - // this.promise = Server.GetField(this.fieldId).then(action((field: any) => { - // this.promise = undefined; - // this.cache = field; - // if (field === undefined) this.failed = true; - // return field; - // })); - this.promise = new Promise(r => r()); - } - callback && this.promise.then(callback); - return null; - } -} - -export type Field = number | string | boolean | ObjectField | RefField; -export type Opt = T | undefined; -export type FieldWaiting = null; -export const FieldWaiting: FieldWaiting = null; - -const Self = Symbol("Self"); - -function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { - if (SerializationHelper.IsSerializing()) { - target[prop] = value; - return true; - } - if (typeof prop === "symbol") { - target[prop] = value; - return true; - } - const curValue = target.__fields[prop]; - if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { - // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically - // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way - return true; - } - if (value instanceof RefField) { - value = new ProxyField(value); - } - if (value instanceof ObjectField) { - if (value[Parent] && value[Parent] !== target) { - throw new Error("Can't put the same object in multiple documents at the same time"); - } - value[Parent] = target; - value[OnUpdate] = (diff?: any) => { - if (!diff) diff = SerializationHelper.Serialize(value); - target[Update]({ [prop]: diff }); - }; - } - if (curValue instanceof ObjectField) { - delete curValue[Parent]; - delete curValue[OnUpdate]; - } - target.__fields[prop] = value; - target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) }); - UndoManager.AddEvent({ - redo: () => receiver[prop] = value, - undo: () => receiver[prop] = curValue - }); - return true; -} - -function getter(target: any, prop: string | symbol | number, receiver: any): any { - if (typeof prop === "symbol") { - return target.__fields[prop] || target[prop]; - } - if (SerializationHelper.IsSerializing()) { - return target[prop]; - } - return getField(target, prop, receiver); -} - -function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { - const field = target.__fields[prop]; - if (field instanceof ProxyField) { - return field.value(callback); - } - if (field === undefined && !ignoreProto) { - const proto = getField(target, "prototype", true); - if (proto instanceof Doc) { - let field = proto[prop]; - callback && callback(field === null ? undefined : field); - return field; - } - } - callback && callback(field); - return field; - -} - -@Deserializable("list") -class ListImpl extends ObjectField { - constructor() { - super(); - const list = new Proxy(this, { - set: function (a, b, c, d) { return setter(a, b, c, d); }, - get: getter, - deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, - defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, - }); - return list; - } - - [key: number]: T | null | undefined; - - @serializable(alias("fields", list(autoObject()))) - @observable - private __fields: (T | null | undefined)[] = []; - - private [Update] = (diff: any) => { - console.log(diff); - const update = this[OnUpdate]; - update && update(diff); - } - - private [Self] = this; -} -export type List = ListImpl & T[]; -export const List: { new (): List } = ListImpl as any; - -@Deserializable("doc").withFields(["id"]) -export class Doc extends RefField { - constructor(id?: string, forceSave?: boolean) { - super(id); - const doc = new Proxy(this, { - set: setter, - get: getter, - deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, - defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, - }); - if (!id || forceSave) { - DocServer.CreateField(SerializationHelper.Serialize(doc)); - } - return doc; - } - - [key: string]: Field | null | undefined; - - @serializable(alias("fields", map(autoObject()))) - @observable - private __fields: { [key: string]: Field | null | undefined } = {}; - - private [Update] = (diff: any) => { - DocServer.UpdateField(this[Id], diff); - } - - private [Self] = this; -} - -export namespace Doc { - export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise { - const self = doc[Self]; - return new Promise(res => getField(self, key, ignoreProto, res)); - } - export function GetTAsync(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Promise { - const self = doc[Self]; - return new Promise(async res => { - const field = await GetAsync(doc, key, ignoreProto); - return Cast(field, ctor); - }); - } - export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined { - const self = doc[Self]; - return getField(self, key, ignoreProto); - } - export function GetT(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Field | null | undefined { - return Cast(Get(doc, key, ignoreProto), ctor); - } - export const Prototype = Symbol("Prototype"); -} - -export const GetAsync = Doc.GetAsync; - -export type ToType = - T extends "string" ? string : - T extends "number" ? number : - T extends "boolean" ? boolean : - T extends ListSpec ? List : - T extends { new(...args: any[]): infer R } ? R : never; - -export type ToConstructor = - T extends string ? "string" : - T extends number ? "number" : - T extends boolean ? "boolean" : { new(...args: any[]): T }; - -export type ToInterface = { - [P in keyof T]: ToType; -}; - -// type ListSpec = { List: FieldCtor> | ListSpec> }; -export type ListSpec = { List: FieldCtor }; - -// type ListType = { 0: List>>, 1: ToType> }[HasTail extends true ? 0 : 1]; - -type Head = T extends [any, ...any[]] ? T[0] : never; -type Tail = - ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []; -type HasTail = T extends ([] | [any]) ? false : true; - -interface Interface { - [key: string]: ToConstructor | ListSpec; - // [key: string]: ToConstructor | ListSpec; -} - -type FieldCtor = ToConstructor | ListSpec; - -function Cast>(field: Field | null | undefined, ctor: T): ToType | null | undefined { - if (field !== undefined && field !== null) { - if (typeof ctor === "string") { - if (typeof field === ctor) { - return field as ToType; - } - } else if (typeof ctor === "object") { - if (field instanceof List) { - return field as ToType; - } - } else if (field instanceof (ctor as any)) { - return field as ToType; - } - } else { - return field; - } - return undefined; -} - -export type makeInterface = Partial> & Doc; -export function makeInterface(schema: T): (doc: Doc) => makeInterface { - return function (doc: any) { - return new Proxy(doc, { - get(target, prop) { - const field = target[prop]; - if (prop in schema) { - return Cast(field, (schema as any)[prop]); - } - return field; - } - }); - }; -} - -export type makeStrictInterface = Partial>; -export function makeStrictInterface(schema: T): (doc: Doc) => makeStrictInterface { - const proto = {}; - for (const key in schema) { - const type = schema[key]; - Object.defineProperty(proto, key, { - get() { - return Cast(this.__doc[key], type as any); - }, - set(value) { - value = Cast(value, type as any); - if (value !== undefined) { - this.__doc[key] = value; - return; - } - throw new TypeError("Expected type " + type); - } - }); - } - return function (doc: any) { - const obj = Object.create(proto); - obj.__doc = doc; - return obj; - }; -} - -export function createSchema(schema: T): T { - return schema; -} \ No newline at end of file diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts deleted file mode 100644 index 347f1fa05..000000000 --- a/src/fields/TupleField.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx"; -import { Server } from "../client/Server"; -import { UndoManager } from "../client/util/UndoManager"; -import { Types } from "../server/Message"; -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; - -export class TupleField extends BasicField<[T, U]> { - constructor(data: [T, U], id?: FieldId, save: boolean = true) { - super(data, save, id); - if (save) { - Server.UpdateField(this); - } - this.observeTuple(); - } - - private observeDisposer: Lambda | undefined; - private observeTuple(): void { - this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray, (change: IArrayChange | IArraySplice) => { - if (change.type === "update") { - UndoManager.AddEvent({ - undo: () => this.Data[change.index] = change.oldValue, - redo: () => this.Data[change.index] = change.newValue - }); - Server.UpdateField(this); - } else { - throw new Error("Why are you messing with the length of a tuple, huh?"); - } - }); - } - - protected setData(value: [T, U]) { - if (this.observeDisposer) { - this.observeDisposer(); - } - this.data = observable(value) as (T | U)[] as [T, U]; - this.observeTuple(); - } - - UpdateFromServer(values: [T, U]) { - this.setData(values); - } - - ToScriptString(): string { - return `new TupleField([${this.Data[0], this.Data[1]}])`; - } - - Copy(): Field { - return new TupleField(this.Data); - } - - ToJson() { - return { - type: Types.Tuple, - data: this.Data, - id: this.Id - }; - } -} \ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts new file mode 100644 index 000000000..c67170573 --- /dev/null +++ b/src/new_fields/Doc.ts @@ -0,0 +1,90 @@ +import { observable, action } from "mobx"; +import { serializable, primitive, map, alias, list } from "serializr"; +import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; +import { Utils } from "../Utils"; +import { DocServer } from "../client/DocServer"; +import { setter, getter, getField } from "./util"; +import { Cast, FieldCtor } from "./Types"; + +export const HandleUpdate = Symbol("HandleUpdate"); +export const Id = Symbol("Id"); +export abstract class RefField { + @serializable(alias("id", primitive())) + private __id: string; + readonly [Id]: string; + + constructor(id?: string) { + this.__id = id || Utils.GenerateGuid(); + this[Id] = this.__id; + } + + protected [HandleUpdate]?(diff: any): void; +} + +export const Update = Symbol("Update"); +export const OnUpdate = Symbol("OnUpdate"); +export const Parent = Symbol("Parent"); +export class ObjectField { + protected [OnUpdate]?: (diff?: any) => void; + private [Parent]?: Doc; +} + +export type Field = number | string | boolean | ObjectField | RefField; +export type Opt = T | undefined; +export type FieldWaiting = null; +export const FieldWaiting: FieldWaiting = null; + +export const Self = Symbol("Self"); + +@Deserializable("doc").withFields(["id"]) +export class Doc extends RefField { + constructor(id?: string, forceSave?: boolean) { + super(id); + const doc = new Proxy(this, { + set: setter, + get: getter, + deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, + defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, + }); + if (!id || forceSave) { + DocServer.CreateField(SerializationHelper.Serialize(doc)); + } + return doc; + } + + [key: string]: Field | null | undefined; + + @serializable(alias("fields", map(autoObject()))) + @observable + private __fields: { [key: string]: Field | null | undefined } = {}; + + private [Update] = (diff: any) => { + DocServer.UpdateField(this[Id], diff); + } + + private [Self] = this; +} + +export namespace Doc { + export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise { + const self = doc[Self]; + return new Promise(res => getField(self, key, ignoreProto, res)); + } + export function GetTAsync(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Promise { + const self = doc[Self]; + return new Promise(async res => { + const field = await GetAsync(doc, key, ignoreProto); + return Cast(field, ctor); + }); + } + export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined { + const self = doc[Self]; + return getField(self, key, ignoreProto); + } + export function GetT(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Field | null | undefined { + return Cast(Get(doc, key, ignoreProto), ctor); + } + export const Prototype = Symbol("Prototype"); +} + +export const GetAsync = Doc.GetAsync; \ No newline at end of file diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts new file mode 100644 index 000000000..f8e54ade5 --- /dev/null +++ b/src/new_fields/HtmlField.ts @@ -0,0 +1,14 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { serializable, primitive } from "serializr"; +import { ObjectField } from "./Doc"; + +@Deserializable("html") +export class URLField extends ObjectField { + @serializable(primitive()) + readonly html: string; + + constructor(html: string) { + super(); + this.html = html; + } +} diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts new file mode 100644 index 000000000..a1a623f83 --- /dev/null +++ b/src/new_fields/List.ts @@ -0,0 +1,35 @@ +import { Deserializable, autoObject } from "../client/util/SerializationHelper"; +import { Field, ObjectField, Update, OnUpdate, Self } from "./Doc"; +import { setter, getter } from "./util"; +import { serializable, alias, list } from "serializr"; +import { observable } from "mobx"; + +@Deserializable("list") +class ListImpl extends ObjectField { + constructor() { + super(); + const list = new Proxy(this, { + set: function (a, b, c, d) { return setter(a, b, c, d); }, + get: getter, + deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, + defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, + }); + return list; + } + + [key: number]: T | null | undefined; + + @serializable(alias("fields", list(autoObject()))) + @observable + private __fields: (T | null | undefined)[] = []; + + private [Update] = (diff: any) => { + console.log(diff); + const update = this[OnUpdate]; + update && update(diff); + } + + private [Self] = this; +} +export type List = ListImpl & T[]; +export const List: { new (): List } = ListImpl as any; \ No newline at end of file diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts new file mode 100644 index 000000000..3b4b2e452 --- /dev/null +++ b/src/new_fields/Proxy.ts @@ -0,0 +1,55 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { RefField, Id, ObjectField } from "./Doc"; +import { primitive, serializable } from "serializr"; +import { observable } from "mobx"; + +@Deserializable("proxy") +export class ProxyField extends ObjectField { + constructor(); + constructor(value: T); + constructor(value?: T) { + super(); + if (value) { + this.cache = value; + this.fieldId = value[Id]; + } + } + + @serializable(primitive()) + readonly fieldId: string = ""; + + // This getter/setter and nested object thing is + // because mobx doesn't play well with observable proxies + @observable.ref + private _cache: { readonly field: T | undefined } = { field: undefined }; + private get cache(): T | undefined { + return this._cache.field; + } + private set cache(field: T | undefined) { + this._cache = { field }; + } + + private failed = false; + private promise?: Promise; + + value(callback?: ((field: T | undefined) => void)): T | undefined | null { + if (this.cache) { + callback && callback(this.cache); + return this.cache; + } + if (this.failed) { + return undefined; + } + if (!this.promise) { + // this.promise = Server.GetField(this.fieldId).then(action((field: any) => { + // this.promise = undefined; + // this.cache = field; + // if (field === undefined) this.failed = true; + // return field; + // })); + this.promise = new Promise(r => r()); + } + callback && this.promise.then(callback); + return null; + } +} diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts new file mode 100644 index 000000000..c7d2f0801 --- /dev/null +++ b/src/new_fields/Schema.ts @@ -0,0 +1,47 @@ +import { Interface, ToInterface, Cast } from "./Types"; +import { Doc } from "./Doc"; + +export type makeInterface = Partial> & Doc; +export function makeInterface(schema: T): (doc: Doc) => makeInterface { + return function (doc: any) { + return new Proxy(doc, { + get(target, prop) { + const field = target[prop]; + if (prop in schema) { + return Cast(field, (schema as any)[prop]); + } + return field; + } + }); + }; +} + +export type makeStrictInterface = Partial>; +export function makeStrictInterface(schema: T): (doc: Doc) => makeStrictInterface { + const proto = {}; + for (const key in schema) { + const type = schema[key]; + Object.defineProperty(proto, key, { + get() { + return Cast(this.__doc[key], type as any); + }, + set(value) { + value = Cast(value, type as any); + if (value !== undefined) { + this.__doc[key] = value; + return; + } + throw new TypeError("Expected type " + type); + } + }); + } + return function (doc: any) { + const obj = Object.create(proto); + obj.__doc = doc; + return obj; + }; +} + +export function createSchema(schema: T): T { + return schema; +} diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts new file mode 100644 index 000000000..416298a64 --- /dev/null +++ b/src/new_fields/Types.ts @@ -0,0 +1,58 @@ +import { Field, Opt } from "./Doc"; +import { List } from "./List"; + +export type ToType = + T extends "string" ? string : + T extends "number" ? number : + T extends "boolean" ? boolean : + T extends ListSpec ? List : + T extends { new(...args: any[]): infer R } ? R : never; + +export type ToConstructor = + T extends string ? "string" : + T extends number ? "number" : + T extends boolean ? "boolean" : { new(...args: any[]): T }; + +export type ToInterface = { + [P in keyof T]: ToType; +}; + +// type ListSpec = { List: FieldCtor> | ListSpec> }; +export type ListSpec = { List: FieldCtor }; + +// type ListType = { 0: List>>, 1: ToType> }[HasTail extends true ? 0 : 1]; + +export type Head = T extends [any, ...any[]] ? T[0] : never; +export type Tail = + ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []; +export type HasTail = T extends ([] | [any]) ? false : true; + +export interface Interface { + [key: string]: ToConstructor | ListSpec; + // [key: string]: ToConstructor | ListSpec; +} + +export type FieldCtor = ToConstructor | ListSpec; + +export function Cast>(field: Field | null | undefined, ctor: T): ToType | null | undefined { + if (field !== undefined && field !== null) { + if (typeof ctor === "string") { + if (typeof field === ctor) { + return field as ToType; + } + } else if (typeof ctor === "object") { + if (field instanceof List) { + return field as ToType; + } + } else if (field instanceof (ctor as any)) { + return field as ToType; + } + } else { + return field; + } + return undefined; +} + +export function FieldValue(field: Opt | Promise>): Opt { + return field instanceof Promise ? undefined : field; +} diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts new file mode 100644 index 000000000..d27a2b692 --- /dev/null +++ b/src/new_fields/URLField.ts @@ -0,0 +1,25 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { serializable } from "serializr"; +import { ObjectField } from "./Doc"; + +function url() { + return { + serializer: function (value: URL) { + return value.href; + }, + deserializer: function (jsonValue: string, done: (err: any, val: any) => void) { + done(undefined, new URL(jsonValue)); + } + }; +} + +@Deserializable("url") +export class URLField extends ObjectField { + @serializable(url()) + readonly url: URL; + + constructor(url: URL) { + super(); + this.url = url; + } +} \ No newline at end of file diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts new file mode 100644 index 000000000..0f08ecf03 --- /dev/null +++ b/src/new_fields/util.ts @@ -0,0 +1,73 @@ +import { UndoManager } from "../client/util/UndoManager"; +import { Update, OnUpdate, Parent, ObjectField, RefField, Doc, Id, Field } from "./Doc"; +import { SerializationHelper } from "../client/util/SerializationHelper"; +import { ProxyField } from "./Proxy"; + +export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { + if (SerializationHelper.IsSerializing()) { + target[prop] = value; + return true; + } + if (typeof prop === "symbol") { + target[prop] = value; + return true; + } + const curValue = target.__fields[prop]; + if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { + // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically + // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way + return true; + } + if (value instanceof RefField) { + value = new ProxyField(value); + } + if (value instanceof ObjectField) { + if (value[Parent] && value[Parent] !== target) { + throw new Error("Can't put the same object in multiple documents at the same time"); + } + value[Parent] = target; + value[OnUpdate] = (diff?: any) => { + if (!diff) diff = SerializationHelper.Serialize(value); + target[Update]({ [prop]: diff }); + }; + } + if (curValue instanceof ObjectField) { + delete curValue[Parent]; + delete curValue[OnUpdate]; + } + target.__fields[prop] = value; + target[Update]({ ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) }); + UndoManager.AddEvent({ + redo: () => receiver[prop] = value, + undo: () => receiver[prop] = curValue + }); + return true; +} + +export function getter(target: any, prop: string | symbol | number, receiver: any): any { + if (typeof prop === "symbol") { + return target.__fields[prop] || target[prop]; + } + if (SerializationHelper.IsSerializing()) { + return target[prop]; + } + return getField(target, prop, receiver); +} + +export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { + const field = target.__fields[prop]; + if (field instanceof ProxyField) { + return field.value(callback); + } + if (field === undefined && !ignoreProto) { + const proto = getField(target, "prototype", true); + if (proto instanceof Doc) { + let field = proto[prop]; + callback && callback(field === null ? undefined : field); + return field; + } + } + callback && callback(field); + return field; + +} -- cgit v1.2.3-70-g09d2 From e3ad2c2c8f920a5538541b8e495af52815e36dc6 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Apr 2019 23:52:34 -0400 Subject: Started fixing stuff --- src/client/views/DocComponent.tsx | 14 ++++++ src/client/views/nodes/DocumentView.tsx | 87 ++++++++++----------------------- src/new_fields/Doc.ts | 8 +++ src/new_fields/Schema.ts | 7 +-- src/new_fields/Types.ts | 2 +- 5 files changed, 54 insertions(+), 64 deletions(-) create mode 100644 src/client/views/DocComponent.tsx (limited to 'src') diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx new file mode 100644 index 000000000..31282744b --- /dev/null +++ b/src/client/views/DocComponent.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Doc } from '../../new_fields/Doc'; +import { computed } from 'mobx'; + +export function DocComponent

(schemaCtor: (doc: Doc) => T) { + class Component extends React.Component

{ + //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then + @computed + get Document() { + return schemaCtor(this.props.Document); + } + } + return Component; +} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b99e449be..4f7909692 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,12 +1,5 @@ import { action, computed, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { BooleanField } from "../../../fields/BooleanField"; -import { Document } from "../../../fields/Document"; -import { Field, FieldWaiting, Opt } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import { TextField } from "../../../fields/TextField"; import { ServerUtils } from "../../../server/ServerUtil"; import { emptyFunction, Utils } from "../../../Utils"; import { Documents } from "../../documents/Documents"; @@ -23,11 +16,15 @@ import { ContextMenu } from "../ContextMenu"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); +import { Field, Opt, Doc, Id } from "../../../new_fields/Doc"; +import { DocComponent } from "../DocComponent"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { FieldValue } from "../../../new_fields/Types"; export interface DocumentViewProps { ContainingCollectionView: Opt; - Document: Document; + Document: Doc; addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Document) => boolean; moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; @@ -41,48 +38,20 @@ export interface DocumentViewProps { parentActive: () => boolean; onActiveChanged: (isActive: boolean) => void; } -export interface JsxArgs extends DocumentViewProps { - Keys: { [name: string]: Key }; - Fields: { [name: string]: Field }; -} -/* -This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that -jsx-to-string can recover the jsx from -Example usage of this function: - public static LayoutString() { - let args = FakeJsxArgs(["Data"]); - return jsxToString( - , - { useFunctionCode: true, functionNameOnly: true } - ) - } -*/ -export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { - let Keys: { [name: string]: any } = {}; - let Fields: { [name: string]: any } = {}; - for (const key of keys) { - Object.defineProperty(emptyFunction, "name", { value: key + "Key" }); - Keys[key] = emptyFunction; - } - for (const field of fields) { - Object.defineProperty(emptyFunction, "name", { value: field }); - Fields[field] = emptyFunction; - } - let args: JsxArgs = { - Document: function Document() { }, - DocumentView: function DocumentView() { }, - Keys, - Fields - } as any; - return args; -} +const schema = createSchema({ + layout: "string", + minimized: "boolean", + nativeWidth: "number", + nativeHeight: "number", + backgroundColor: "string" +}); + +type Document = makeInterface; +const Document = makeInterface(schema); @observer -export class DocumentView extends React.Component { +export class DocumentView extends DocComponent(Document) { private _downX: number = 0; private _downY: number = 0; private _mainCont = React.createRef(); @@ -91,9 +60,6 @@ export class DocumentView extends React.Component { public get ContentDiv() { return this._mainCont.current; } @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } @computed get topMost(): boolean { return this.props.isTopMost; } - @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "

Error loading layout data

"); } - @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array()); } - @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array()); } onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; @@ -200,7 +166,7 @@ export class DocumentView extends React.Component { } } fullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.OpenFullScreen((this.props.Document.GetPrototype() as Document).MakeDelegate()); + CollectionDockingView.Instance.OpenFullScreen(Doc.MakeDelegate(this.Document.prototype)); ContextMenu.Instance.clearItems(); ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); @@ -266,9 +232,9 @@ export class DocumentView extends React.Component { onDrop = (e: React.DragEvent) => { let text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith(" { ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }); ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)) }); - ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document.Id) }); + ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) }); //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - if (!SelectionManager.IsSelected(this)) + if (!SelectionManager.IsSelected(this)) { SelectionManager.SelectDoc(this, false); + } } @action - expand = () => this.props.Document.SetBoolean(KeyStore.Minimized, false) - isMinimized = () => this.props.Document.GetBoolean(KeyStore.Minimized, false); + expand = () => this.Document.minimized = false + isMinimized = () => FieldValue(this.Document.minimized) || false; isSelected = () => SelectionManager.IsSelected(this); select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth) || 0; } + @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight) || 0; } @computed get contents() { return (); } render() { @@ -321,7 +288,7 @@ export class DocumentView extends React.Component {
(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Field | null | undefined { return Cast(Get(doc, key, ignoreProto), ctor); } + export function MakeDelegate(doc: Opt): Opt { + if (!doc) { + return undefined; + } + const delegate = new Doc(); + delegate.prototype = doc; + return delegate; + } export const Prototype = Symbol("Prototype"); } diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index c7d2f0801..27b9635af 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -1,4 +1,4 @@ -import { Interface, ToInterface, Cast } from "./Types"; +import { Interface, ToInterface, Cast, FieldCtor, ToConstructor } from "./Types"; import { Doc } from "./Doc"; export type makeInterface = Partial> & Doc; @@ -42,6 +42,7 @@ export function makeStrictInterface(schema: T): (doc: Doc) }; } -export function createSchema(schema: T): T { - return schema; +export function createSchema(schema: T): T & { prototype: ToConstructor } { + schema.prototype = Doc; + return schema as any; } diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 416298a64..cafb208ce 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -53,6 +53,6 @@ export function Cast>(field: Field | null | undefined return undefined; } -export function FieldValue(field: Opt | Promise>): Opt { +export function FieldValue(field: Opt | Promise>): Opt { return field instanceof Promise ? undefined : field; } -- cgit v1.2.3-70-g09d2 From 1eb965a5d9c8aaebf1970bc645edecfb7017b601 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 20 Apr 2019 03:29:35 -0400 Subject: Made the switch in a couple more classes --- src/Utils.ts | 3 -- src/client/views/nodes/DocumentContentsView.tsx | 26 ++------------ src/client/views/nodes/DocumentView.tsx | 16 +++++++-- src/client/views/nodes/FieldView.tsx | 45 ++++++++++--------------- src/client/views/nodes/ImageBox.tsx | 41 +++++++++++++--------- src/debug/Test.tsx | 4 +-- src/new_fields/Doc.ts | 5 +-- src/new_fields/HtmlField.ts | 2 +- src/new_fields/Schema.ts | 24 +++++++++++-- src/new_fields/Types.ts | 19 +++++++---- 10 files changed, 98 insertions(+), 87 deletions(-) (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index 98f75d3b9..066d653f5 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -2,7 +2,6 @@ import v4 = require('uuid/v4'); import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; import { Message } from './server/Message'; -import { Document } from './fields/Document'; export class Utils { @@ -108,6 +107,4 @@ export function returnZero() { return 0; } export function emptyFunction() { } -export function emptyDocFunction(doc: Document) { } - export type Without = Pick>; \ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 07599c345..273401111 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,9 +1,5 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { FieldWaiting, Field } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; @@ -22,46 +18,30 @@ import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); -import { Document } from "../../../fields/Document"; import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; +import { Cast } from "../../../new_fields/Types"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without; export interface JsxBindings { props: BindingProps; - [keyName: string]: BindingProps | Field; } @observer export class DocumentContentsView extends React.Component boolean, select: (ctrl: boolean) => void, - layoutKey: Key + layoutKey: string }> { - @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "

Error loading layout data

"); } - @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array()); } - @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array()); } - + @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "

Error loading layout data

"); } CreateBindings(): JsxBindings { let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive) }; - - for (const key of this.layoutKeys) { - bindings[key.Name + "Key"] = key; // this maps string values of the form Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data - } - for (const key of this.layoutFields) { - let field = this.props.Document.Get(key); - bindings[key.Name] = field && field !== FieldWaiting ? field.GetValue() : field; - } return bindings; } render() { - let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); - if (!lkeys || lkeys === FieldWaiting) { - return

Error loading layout keys

; - } return ; -const Document = makeInterface(schema); +export const positionSchema = createSchema({ + nativeWidth: "number", + nativeHeight: "number", + width: "number", + height: "number", + x: "number", + y: "number", +}); + +type Document = makeInterface<[typeof schema]>; +const Document = makeInterface([schema]); @observer export class DocumentView extends DocComponent(Document) { @@ -146,7 +155,7 @@ export class DocumentView extends DocComponent(Docu document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); e.stopPropagation(); - if (!SelectionManager.IsSelected(this) && e.button !== 2) + if (!SelectionManager.IsSelected(this) && e.button !== 2) { if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { if (this.props.Document.Get(KeyStore.MaximizedDoc) instanceof Document) { this.props.Document.GetAsync(KeyStore.MaximizedDoc, maxdoc => { @@ -159,6 +168,7 @@ export class DocumentView extends DocComponent(Docu SelectionManager.SelectDoc(this, e.ctrlKey); } } + } } stopPropagation = (e: React.SyntheticEvent) => { e.stopPropagation(); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 93e385821..d8de5afec 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,30 +1,21 @@ import React = require("react"); import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Field, FieldWaiting, FieldValue, Opt } from "../../../fields/Field"; -import { Document } from "../../../fields/Document"; -import { TextField } from "../../../fields/TextField"; -import { NumberField } from "../../../fields/NumberField"; -import { RichTextField } from "../../../fields/RichTextField"; -import { ImageField } from "../../../fields/ImageField"; -import { VideoField } from "../../../fields/VideoField"; -import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { WebBox } from "./WebBox"; import { VideoBox } from "./VideoBox"; import { AudioBox } from "./AudioBox"; -import { AudioField } from "../../../fields/AudioField"; -import { ListField } from "../../../fields/ListField"; import { DocumentContentsView } from "./DocumentContentsView"; import { Transform } from "../../util/Transform"; -import { KeyStore } from "../../../fields/KeyStore"; -import { returnFalse, emptyDocFunction, emptyFunction, returnOne } from "../../../Utils"; +import { returnFalse, emptyFunction, returnOne } from "../../../Utils"; import { CollectionView } from "../collections/CollectionView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { IconField } from "../../../fields/IconFIeld"; import { IconBox } from "./IconBox"; +import { Opt, Doc, Field, FieldValue, FieldWaiting } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; // @@ -33,9 +24,9 @@ import { IconBox } from "./IconBox"; // See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc. // export interface FieldViewProps { - fieldKey: Key; + fieldKey: string; ContainingCollectionView: Opt; - Document: Document; + Document: Doc; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; isTopMost: boolean; @@ -56,17 +47,17 @@ export class FieldView extends React.Component { } @computed - get field(): FieldValue { - const { Document: doc, fieldKey } = this.props; - return doc.Get(fieldKey); + get field(): FieldValue { + const { Document, fieldKey } = this.props; + return Document[fieldKey]; } render() { const field = this.field; if (!field) { return

{''}

; } - if (field instanceof TextField) { - return

{field.Data}

; + if (typeof field === "string") { + return

{field}

; } else if (field instanceof RichTextField) { return ; @@ -83,7 +74,7 @@ export class FieldView extends React.Component { else if (field instanceof AudioField) { return ; } - else if (field instanceof Document) { + else if (field instanceof Doc) { return ( { PanelHeight={() => 100} isTopMost={true} //TODO Why is this top most? selectOnLoad={false} - focus={emptyDocFunction} + focus={emptyFunction} isSelected={returnFalse} select={returnFalse} - layoutKey={KeyStore.Layout} + layoutKey={"layout"} ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} /> ); } - else if (field instanceof ListField) { + else if (field instanceof List) { return (
- {(field as ListField).Data.map(f => f instanceof Document ? f.Title : f.GetValue().toString()).join(", ")} + {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")}
); } // bcz: this belongs here, but it doesn't render well so taking it out for now // else if (field instanceof HtmlField) { // return // } - else if (field instanceof NumberField) { - return

{field.Data}

; + else if (typeof field === "number") { + return

{field}

; } else if (field !== FieldWaiting) { - return

{JSON.stringify(field.GetValue())}

; + return

{JSON.stringify(field)}

; } else { return

{"Waiting for server..."}

; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index fd5381b77..3b72e9f39 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -3,11 +3,6 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { Document } from '../../../fields/Document'; -import { FieldWaiting } from '../../../fields/Field'; -import { ImageField } from '../../../fields/ImageField'; -import { KeyStore } from '../../../fields/KeyStore'; -import { ListField } from '../../../fields/ListField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -15,9 +10,23 @@ import { ContextMenu } from "../../views/ContextMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { DocComponent } from '../DocComponent'; +import { positionSchema } from './DocumentView'; +import { FieldValue, Cast } from '../../../new_fields/Types'; +import { URLField } from '../../../new_fields/URLField'; +import { Doc, FieldWaiting } from '../../../new_fields/Doc'; +import { List } from '../../../new_fields/List'; + +const schema = createSchema({ + curPage: "number" +}); + +type ImageDocument = makeInterface<[typeof schema, typeof positionSchema]>; +const ImageDocument = makeInterface([schema, positionSchema]); @observer -export class ImageBox extends React.Component { +export class ImageBox extends DocComponent(ImageDocument) { public static LayoutString() { return FieldView.LayoutString(ImageBox); } private _imgRef: React.RefObject; @@ -39,8 +48,8 @@ export class ImageBox extends React.Component { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; if (this._photoIndex === 0) { - this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); - this.props.Document.SetNumber(KeyStore.Height, this.props.Document.Width() * h / w); + this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w; + this.Document.height = FieldValue(this.Document.width, 0) * h / w; } } @@ -126,9 +135,9 @@ export class ImageBox extends React.Component { } specificContextMenu = (e: React.MouseEvent): void => { - let field = this.props.Document.GetT(this.props.fieldKey, ImageField); + let field = Cast(this.Document[this.props.fieldKey], URLField); if (field && field !== FieldWaiting) { - let url = field.Data.href; + let url = field.url.href; ContextMenu.Instance.addItem({ description: "Copy path", event: () => { Utils.CopyText(url); @@ -140,11 +149,11 @@ export class ImageBox extends React.Component { @action onDotDown(index: number) { this._photoIndex = index; - this.props.Document.SetNumber(KeyStore.CurPage, index); + this.Document.curPage = index; } dots(paths: string[]) { - let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); + let nativeWidth = FieldValue(this.Document.nativeWidth, 1); let dist = Math.min(nativeWidth / paths.length, 40); let left = (nativeWidth - paths.length * dist) / 2; return paths.map((p, i) => @@ -155,12 +164,12 @@ export class ImageBox extends React.Component { } render() { - let field = this.props.Document.Get(this.props.fieldKey); + let field = this.Document[this.props.fieldKey]; let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; if (field === FieldWaiting) paths = ["https://image.flaticon.com/icons/svg/66/66163.svg"]; - else if (field instanceof ImageField) paths = [field.Data.href]; - else if (field instanceof ListField) paths = field.Data.filter(val => val as ImageField).map(p => (p as ImageField).Data.href); - let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); + else if (field instanceof URLField) paths = [field.url.href]; + else if (field instanceof List) paths = field.filter(val => val instanceof URLField).map(p => (p as URLField).url.href); + let nativeWidth = FieldValue(this.Document.nativeWidth, 1); return (
Image not found diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 8b9c9fa0b..81c69ca8e 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -16,8 +16,8 @@ const schema1 = createSchema({ testDoc: Doc }); -const TestDoc = makeInterface(schema1); -type TestDoc = makeInterface; +const TestDoc = makeInterface([schema1]); +type TestDoc = makeInterface<[typeof schema1]>; const schema2 = createSchema({ hello: URLField, diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 23a8c05cc..e0eb44ee9 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -33,6 +33,7 @@ export type Field = number | string | boolean | ObjectField | RefField; export type Opt = T | undefined; export type FieldWaiting = null; export const FieldWaiting: FieldWaiting = null; +export type FieldValue = Opt | FieldWaiting; export const Self = Symbol("Self"); @@ -81,8 +82,8 @@ export namespace Doc { const self = doc[Self]; return getField(self, key, ignoreProto); } - export function GetT(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Field | null | undefined { - return Cast(Get(doc, key, ignoreProto), ctor); + export function GetT(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): T | null | undefined { + return Cast(Get(doc, key, ignoreProto), ctor) as T | null | undefined; } export function MakeDelegate(doc: Opt): Opt { if (!doc) { diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts index f8e54ade5..76fdb1f62 100644 --- a/src/new_fields/HtmlField.ts +++ b/src/new_fields/HtmlField.ts @@ -3,7 +3,7 @@ import { serializable, primitive } from "serializr"; import { ObjectField } from "./Doc"; @Deserializable("html") -export class URLField extends ObjectField { +export class HtmlField extends ObjectField { @serializable(primitive()) readonly html: string; diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index 1607d4c15..1d1f56844 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -1,8 +1,26 @@ -import { Interface, ToInterface, Cast, FieldCtor, ToConstructor } from "./Types"; +import { Interface, ToInterface, Cast, FieldCtor, ToConstructor, HasTail, Head, Tail } from "./Types"; import { Doc } from "./Doc"; -export type makeInterface = Partial> & U; -export function makeInterface(schema: T): (doc: U) => makeInterface { +type All = { + 1: makeInterface, U> & All, U>, + 0: makeInterface, U> +}[HasTail extends true ? 1 : 0]; + +type AllToInterface = { + 1: ToInterface> & AllToInterface>, + 0: ToInterface> +}[HasTail extends true ? 1 : 0]; + +export type makeInterface = Partial> & U; +// export function makeInterface(schemas: T): (doc: U) => All; +// export function makeInterface(schema: T): (doc: U) => makeInterface; +export function makeInterface(schemas: T): (doc: U) => All { + let schema: Interface = {}; + for (const s of schemas) { + for (const key in s) { + schema[key] = s[key]; + } + } return function (doc: any) { return new Proxy(doc, { get(target, prop) { diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index cafb208ce..6ffb3e02f 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -27,14 +27,17 @@ export type Tail = ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []; export type HasTail = T extends ([] | [any]) ? false : true; +//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial export interface Interface { [key: string]: ToConstructor | ListSpec; // [key: string]: ToConstructor | ListSpec; } -export type FieldCtor = ToConstructor | ListSpec; +export type FieldCtor = T extends List ? ListSpec : ToConstructor; -export function Cast>(field: Field | null | undefined, ctor: T): ToType | null | undefined { +export function Cast>(field: Field | null | undefined, ctor: T): ToType | null | undefined; +export function Cast>(field: Field | null | undefined, ctor: T, defaultVal: ToType): ToType; +export function Cast>(field: Field | null | undefined, ctor: T, defaultVal?: ToType): ToType | null | undefined { if (field !== undefined && field !== null) { if (typeof ctor === "string") { if (typeof field === ctor) { @@ -42,17 +45,19 @@ export function Cast>(field: Field | null | undefined } } else if (typeof ctor === "object") { if (field instanceof List) { - return field as ToType; + return field as any; } } else if (field instanceof (ctor as any)) { return field as ToType; } } else { - return field; + return defaultVal; } - return undefined; + return defaultVal; } -export function FieldValue(field: Opt | Promise>): Opt { - return field instanceof Promise ? undefined : field; +export function FieldValue(field: Opt | Promise>, defaultValue: U): T; +export function FieldValue(field: Opt | Promise>): Opt; +export function FieldValue(field: Opt | Promise>, defaultValue?: T): Opt { + return field instanceof Promise ? defaultValue : field; } -- cgit v1.2.3-70-g09d2 From 8ddec1c70c01b3d7d919908299e1168b75698dc7 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 20 Apr 2019 19:06:28 -0400 Subject: More refactoring --- .../views/collections/CollectionDockingView.tsx | 65 +++++++++++----------- src/new_fields/Doc.ts | 13 +++-- src/new_fields/List.ts | 2 +- src/new_fields/Proxy.ts | 20 +++---- src/new_fields/Types.ts | 2 - src/new_fields/URLField.ts | 12 ++-- src/new_fields/util.ts | 4 +- 7 files changed, 59 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e4c647635..b6c87231f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -4,11 +4,8 @@ import 'golden-layout/src/css/goldenlayout-dark-theme.css'; import { action, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; -import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/KeyStore"; import Measure from "react-measure"; -import { FieldId, Opt, Field, FieldWaiting } from "../../../fields/Field"; -import { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne } from "../../../Utils"; +import { Utils, returnTrue, emptyFunction, returnOne } from "../../../Utils"; import { Server } from "../../Server"; import { undoBatch } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; @@ -17,20 +14,22 @@ import React = require("react"); import { SubCollectionViewProps } from "./CollectionSubView"; import { ServerUtils } from "../../../server/ServerUtil"; import { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; -import { TextField } from "../../../fields/TextField"; -import { ListField } from "../../../fields/ListField"; -import { Transform } from '../../util/Transform' +import { Transform } from '../../util/Transform'; +import { Doc, Id, Opt, Field, FieldId } from "../../../new_fields/Doc"; +import { Cast, FieldValue } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { DocServer } from "../../DocServer"; @observer export class CollectionDockingView extends React.Component { public static Instance: CollectionDockingView; - public static makeDocumentConfig(document: Document) { + public static makeDocumentConfig(document: Doc) { return { type: 'react-component', component: 'DocumentFrameRenderer', - title: document.Title, + title: document.title, props: { - documentId: document.Id, + documentId: document[Id], //collectionDockingView: CollectionDockingView.Instance } }; @@ -48,14 +47,14 @@ export class CollectionDockingView extends React.Component this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Document) { + public OpenFullScreen(document: Doc) { let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document)] @@ -83,7 +82,7 @@ export class CollectionDockingView extends React.Component void = () => { if (this._containerRef.current) { reaction( - () => this.props.Document.GetText(KeyStore.Data, ""), + () => Cast(this.props.Document.data, "string", ""), () => { if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { setTimeout(() => this.setupGoldenLayout(), 1); @@ -203,7 +202,7 @@ export class CollectionDockingView extends React.Component) => - (sourceDoc instanceof Document) && DragLinksAsDocuments(tab, x, y, sourceDoc))); + (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc))); } else if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) { e.stopPropagation(); @@ -213,11 +212,11 @@ export class CollectionDockingView extends React.Component) => { - if (f instanceof Document) { + if (f instanceof Doc) { DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, { handlers: { - dragComplete: action(emptyFunction), + dragComplete: emptyFunction, }, hideSource: false }); @@ -235,7 +234,7 @@ export class CollectionDockingView extends React.Component { var json = JSON.stringify(this._goldenLayout.toConfig()); - this.props.Document.SetText(KeyStore.Data, json); + this.props.Document.data = json; } itemDropped = () => { @@ -264,10 +263,9 @@ export class CollectionDockingView extends React.Component${count}
`); tab.element.append(counter); counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction(() => [f.GetT(KeyStore.LinkedFromDocs, ListField), f.GetT(KeyStore.LinkedToDocs, ListField)], - (lists) => { - let count = (lists.length > 0 && lists[0] && lists[0]!.Data ? lists[0]!.Data.length : 0) + - (lists.length > 1 && lists[1] && lists[1]!.Data ? lists[1]!.Data.length : 0); + tab.reactionDisposer = reaction((): [List | null | undefined, List | null | undefined] => [Cast(f.linkedFromDocs, List), Cast(f.linkedToDocs, List)], + ([linkedFrom, linkedTo]) => { + let count = (linkedFrom ? linkedFrom.length : 0) + (linkedTo ? linkedTo.length : 0); counter.innerHTML = count; }); })); @@ -319,19 +317,22 @@ export class DockedFrameRenderer extends React.Component { _mainCont = React.createRef(); @observable private _panelWidth = 0; @observable private _panelHeight = 0; - @observable private _document: Opt; + @observable private _document: Opt; constructor(props: any) { super(props); - Server.GetField(this.props.documentId, action((f: Opt) => this._document = f as Document)); + DocServer.GetRefField(this.props.documentId).then(action((f: Opt) => this._document = f as Doc)); } - nativeWidth = () => this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); - nativeHeight = () => this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); + nativeWidth = () => Cast(this._document!.nativeWidth, "number", this._panelWidth); + nativeHeight = () => Cast(this._document!.nativeHeight, "number", this._panelHeight); contentScaling = () => { - let wscale = this._panelWidth / (this.nativeWidth() ? this.nativeWidth() : this._panelWidth); - if (wscale * this.nativeHeight() > this._panelHeight) - return this._panelHeight / (this.nativeHeight() ? this.nativeHeight() : this._panelHeight); + const nativeH = this.nativeHeight(); + const nativeW = this.nativeWidth(); + let wscale = this._panelWidth / nativeW; + if (wscale * nativeH > this._panelHeight) { + return this._panelHeight / nativeH; + } return wscale; } @@ -349,7 +350,7 @@ export class DockedFrameRenderer extends React.Component { return (
- { selectOnLoad={false} parentActive={returnTrue} whenActiveChanged={emptyFunction} - focus={emptyDocFunction} + focus={emptyFunction} ContainingCollectionView={undefined} />
); } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index e0eb44ee9..8cbd8cf38 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -6,14 +6,15 @@ import { DocServer } from "../client/DocServer"; import { setter, getter, getField } from "./util"; import { Cast, FieldCtor } from "./Types"; +export type FieldId = string; export const HandleUpdate = Symbol("HandleUpdate"); export const Id = Symbol("Id"); export abstract class RefField { @serializable(alias("id", primitive())) - private __id: string; - readonly [Id]: string; + private __id: FieldId; + readonly [Id]: FieldId; - constructor(id?: string) { + constructor(id?: FieldId) { this.__id = id || Utils.GenerateGuid(); this[Id] = this.__id; } @@ -39,7 +40,7 @@ export const Self = Symbol("Self"); @Deserializable("doc").withFields(["id"]) export class Doc extends RefField { - constructor(id?: string, forceSave?: boolean) { + constructor(id?: FieldId, forceSave?: boolean) { super(id); const doc = new Proxy(this, { set: setter, @@ -53,7 +54,7 @@ export class Doc extends RefField { return doc; } - [key: string]: Field | null | undefined; + [key: string]: Field | FieldWaiting | undefined; @serializable(alias("fields", map(autoObject()))) @observable @@ -72,7 +73,6 @@ export namespace Doc { return new Promise(res => getField(self, key, ignoreProto, res)); } export function GetTAsync(doc: Doc, key: string, ctor: FieldCtor, ignoreProto: boolean = false): Promise { - const self = doc[Self]; return new Promise(async res => { const field = await GetAsync(doc, key, ignoreProto); return Cast(field, ctor); @@ -90,6 +90,7 @@ export namespace Doc { return undefined; } const delegate = new Doc(); + //TODO Does this need to be doc[Self]? delegate.prototype = doc; return delegate; } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index a1a623f83..58b252f7b 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -9,7 +9,7 @@ class ListImpl extends ObjectField { constructor() { super(); const list = new Proxy(this, { - set: function (a, b, c, d) { return setter(a, b, c, d); }, + set: setter, get: getter, deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index 3b4b2e452..6a78388c1 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -1,7 +1,8 @@ import { Deserializable } from "../client/util/SerializationHelper"; -import { RefField, Id, ObjectField } from "./Doc"; +import { RefField, Id, ObjectField, FieldWaiting } from "./Doc"; import { primitive, serializable } from "serializr"; -import { observable } from "mobx"; +import { observable, action } from "mobx"; +import { DocServer } from "../client/DocServer"; @Deserializable("proxy") export class ProxyField extends ObjectField { @@ -32,7 +33,7 @@ export class ProxyField extends ObjectField { private failed = false; private promise?: Promise; - value(callback?: ((field: T | undefined) => void)): T | undefined | null { + value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting { if (this.cache) { callback && callback(this.cache); return this.cache; @@ -41,13 +42,12 @@ export class ProxyField extends ObjectField { return undefined; } if (!this.promise) { - // this.promise = Server.GetField(this.fieldId).then(action((field: any) => { - // this.promise = undefined; - // this.cache = field; - // if (field === undefined) this.failed = true; - // return field; - // })); - this.promise = new Promise(r => r()); + this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => { + this.promise = undefined; + this.cache = field; + if (field === undefined) this.failed = true; + return field; + })); } callback && this.promise.then(callback); return null; diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 6ffb3e02f..0fbd8984c 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -50,8 +50,6 @@ export function Cast>(field: Field | null | undefined } else if (field instanceof (ctor as any)) { return field as ToType; } - } else { - return defaultVal; } return defaultVal; } diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts index d27a2b692..e456a7d16 100644 --- a/src/new_fields/URLField.ts +++ b/src/new_fields/URLField.ts @@ -1,16 +1,16 @@ import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable } from "serializr"; +import { serializable, custom } from "serializr"; import { ObjectField } from "./Doc"; function url() { - return { - serializer: function (value: URL) { + return custom( + function (value: URL) { return value.href; }, - deserializer: function (jsonValue: string, done: (err: any, val: any) => void) { - done(undefined, new URL(jsonValue)); + function (jsonValue: string) { + return new URL(jsonValue); } - }; + ); } @Deserializable("url") diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 0f08ecf03..3806044bd 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -22,6 +22,7 @@ export function setter(target: any, prop: string | symbol | number, value: any, value = new ProxyField(value); } if (value instanceof ObjectField) { + //TODO Instead of target, maybe use target[Self] if (value[Parent] && value[Parent] !== target) { throw new Error("Can't put the same object in multiple documents at the same time"); } @@ -51,7 +52,7 @@ export function getter(target: any, prop: string | symbol | number, receiver: an if (SerializationHelper.IsSerializing()) { return target[prop]; } - return getField(target, prop, receiver); + return getField(target, prop); } export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { @@ -69,5 +70,4 @@ export function getField(target: any, prop: string | number, ignoreProto: boolea } callback && callback(field); return field; - } -- cgit v1.2.3-70-g09d2 From 3556aae6d063d8b654509330e70bc67606f16613 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sun, 21 Apr 2019 00:39:09 -0400 Subject: More changes --- .../views/collections/CollectionDockingView.tsx | 38 +++++++++---------- .../views/nodes/CollectionFreeFormDocumentView.tsx | 43 +++++++++++++--------- src/new_fields/Doc.ts | 9 ++--- src/new_fields/Proxy.ts | 2 +- src/new_fields/Types.ts | 22 ++++++++--- 5 files changed, 65 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index b6c87231f..7b204cbdc 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -250,28 +250,26 @@ export class CollectionDockingView extends React.Component { if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt) => { - if (f !== undefined && f instanceof Document) { - f.GetTAsync(KeyStore.Title, TextField, (tfield) => { - if (tfield !== undefined) { - tab.titleElement[0].textContent = f.Title; - } - }); - f.GetTAsync(KeyStore.LinkedFromDocs, ListField).then(lf => - f.GetTAsync(KeyStore.LinkedToDocs, ListField).then(lt => { - let count = (lf ? lf.Data.length : 0) + (lt ? lt.Data.length : 0); - let counter: any = this.htmlToElement(`
${count}
`); - tab.element.append(counter); - counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction((): [List | null | undefined, List | null | undefined] => [Cast(f.linkedFromDocs, List), Cast(f.linkedToDocs, List)], - ([linkedFrom, linkedTo]) => { - let count = (linkedFrom ? linkedFrom.length : 0) + (linkedTo ? linkedTo.length : 0); - counter.innerHTML = count; - }); - })); + DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async f => { + if (f instanceof Doc) { + const tfield = await Cast(f.title, "string"); + if (tfield !== undefined) { + tab.titleElement[0].textContent = f.Title; + } + const lf = await Cast(f.linkedFromDocs, List); + const lt = await Cast(f.linkedToDocs, List); + let count = (lf ? lf.length : 0) + (lt ? lt.length : 0); + let counter: any = this.htmlToElement(`
${count}
`); + tab.element.append(counter); + counter.DashDocId = tab.contentItem.config.props.documentId; + tab.reactionDisposer = reaction((): [List | null | undefined, List | null | undefined] => [lf, lt], + ([linkedFrom, linkedTo]) => { + let count = (linkedFrom ? linkedFrom.length : 0) + (linkedTo ? linkedTo.length : 0); + counter.innerHTML = count; + }); tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; } - })); + }); } tab.closeElement.off('click') //unbind the current click handler .click(function () { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 1d42b3899..27fd25b5c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,18 +1,27 @@ import { computed, trace } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import { NumberField } from "../../../fields/NumberField"; import { Transform } from "../../util/Transform"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; +import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); import { OmitKeys } from "../../../Utils"; +import { DocComponent } from "../DocComponent"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { FieldValue } from "../../../new_fields/Types"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } +const schema = createSchema({ + zoom: "number", + zIndex: "number" +}); + +type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>; +const FreeformDocument = makeInterface([schema, positionSchema]); + @observer -export class CollectionFreeFormDocumentView extends React.Component { +export class CollectionFreeFormDocumentView extends DocComponent(FreeformDocument) { private _mainCont = React.createRef(); @computed @@ -20,36 +29,36 @@ export class CollectionFreeFormDocumentView extends React.Component this.props.ScreenToLocalTransform() diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 8cbd8cf38..10e8fe7ec 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -32,9 +32,8 @@ export class ObjectField { export type Field = number | string | boolean | ObjectField | RefField; export type Opt = T | undefined; -export type FieldWaiting = null; -export const FieldWaiting: FieldWaiting = null; -export type FieldValue = Opt | FieldWaiting; +export type FieldWaiting = Promise; +export type FieldValue = Opt | FieldWaiting; export const Self = Symbol("Self"); @@ -58,7 +57,7 @@ export class Doc extends RefField { @serializable(alias("fields", map(autoObject()))) @observable - private __fields: { [key: string]: Field | null | undefined } = {}; + private __fields: { [key: string]: Field | FieldWaiting | undefined } = {}; private [Update] = (diff: any) => { DocServer.UpdateField(this[Id], diff); @@ -78,7 +77,7 @@ export namespace Doc { return Cast(field, ctor); }); } - export function Get(doc: Doc, key: string, ignoreProto: boolean = false): Field | null | undefined { + export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldValue { const self = doc[Self]; return getField(self, key, ignoreProto); } diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index 6a78388c1..2aa78731e 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -50,6 +50,6 @@ export class ProxyField extends ObjectField { })); } callback && this.promise.then(callback); - return null; + return this.promise; } } diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 0fbd8984c..0392dc2fb 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -1,4 +1,4 @@ -import { Field, Opt } from "./Doc"; +import { Field, Opt, FieldWaiting, FieldValue } from "./Doc"; import { List } from "./List"; export type ToType = @@ -6,7 +6,7 @@ export type ToType = T extends "number" ? number : T extends "boolean" ? boolean : T extends ListSpec ? List : - T extends { new(...args: any[]): infer R } ? R : never; + T extends { new(...args: any[]): infer R } ? (R | Promise) : never; export type ToConstructor = T extends string ? "string" : @@ -35,10 +35,13 @@ export interface Interface { export type FieldCtor = T extends List ? ListSpec : ToConstructor; -export function Cast>(field: Field | null | undefined, ctor: T): ToType | null | undefined; -export function Cast>(field: Field | null | undefined, ctor: T, defaultVal: ToType): ToType; -export function Cast>(field: Field | null | undefined, ctor: T, defaultVal?: ToType): ToType | null | undefined { - if (field !== undefined && field !== null) { +export function Cast>(field: Field | FieldWaiting | undefined, ctor: T): FieldValue>; +export function Cast>(field: Field | FieldWaiting | undefined, ctor: T, defaultVal: ToType): ToType; +export function Cast>(field: Field | FieldWaiting | undefined, ctor: T, defaultVal?: ToType): FieldValue> | undefined { + if (field instanceof Promise) { + return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) : defaultVal; + } + if (field !== undefined && !(field instanceof Promise)) { if (typeof ctor === "string") { if (typeof field === ctor) { return field as ToType; @@ -59,3 +62,10 @@ export function FieldValue(field: Opt | Promise>): Op export function FieldValue(field: Opt | Promise>, defaultValue?: T): Opt { return field instanceof Promise ? defaultValue : field; } + +export interface PromiseLike { + then(callback: (field: Opt | PromiseLike) => void): void; +} +export function PromiseValue(field: FieldValue): PromiseLike> { + return field instanceof Promise ? field : { then(cb: ((field: Opt) => void)) { return cb(field); } }; +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From ab8b4ffd7f3b89600a71d3167490c40a59be433e Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sun, 21 Apr 2019 01:46:02 -0400 Subject: More changes/fixes --- src/client/views/collections/CollectionSubView.tsx | 354 +++++++++++---------- .../collectionFreeForm/CollectionFreeFormView.tsx | 21 +- src/client/views/nodes/FieldView.tsx | 8 +- src/new_fields/Schema.ts | 15 +- src/new_fields/Types.ts | 2 +- 5 files changed, 210 insertions(+), 190 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5c3b2e586..3add288fd 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -18,11 +18,13 @@ import * as rp from 'request-promise'; import { CollectionView } from "./CollectionView"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; +import { Doc } from "../../../new_fields/Doc"; +import { DocComponent } from "../DocComponent"; export interface CollectionViewProps extends FieldViewProps { - addDocument: (document: Document, allowDuplicates?: boolean) => boolean; - removeDocument: (document: Document) => boolean; - moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; PanelWidth: () => number; PanelHeight: () => number; } @@ -33,198 +35,202 @@ export interface SubCollectionViewProps extends CollectionViewProps { export type CursorEntry = TupleField<[string, string], [number, number]>; -export class CollectionSubView extends React.Component { - private dropDisposer?: DragManager.DragDropDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { - if (this.dropDisposer) { - this.dropDisposer(); +export function CollectionSubView(schemaCtor: (doc: Doc) => T) { + class CollectionSubView extends DocComponent(schemaCtor) { + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } } - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + protected CreateDropTarget(ele: HTMLDivElement) { + this.createDropTarget(ele); } - } - protected CreateDropTarget(ele: HTMLDivElement) { - this.createDropTarget(ele); - } - @action - protected setCursorPosition(position: [number, number]) { - let ind; - let doc = this.props.Document; - let id = CurrentUserUtils.id; - let email = CurrentUserUtils.email; - if (id && email) { - let textInfo: [string, string] = [id, email]; - doc.GetTAsync(KeyStore.Prototype, Document).then(proto => { - if (!proto) { - return; - } - proto.GetOrCreateAsync>(KeyStore.Cursors, ListField, action((field: ListField) => { - let cursors = field.Data; - if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) { - cursors[ind].Data[1] = position; - } else { - let entry = new TupleField<[string, string], [number, number]>([textInfo, position]); - cursors.push(entry); + @action + protected setCursorPosition(position: [number, number]) { + let ind; + let doc = this.props.Document; + let id = CurrentUserUtils.id; + let email = CurrentUserUtils.email; + if (id && email) { + let textInfo: [string, string] = [id, email]; + doc.GetTAsync(KeyStore.Prototype, Document).then(proto => { + if (!proto) { + return; } - })); - }); + proto.GetOrCreateAsync>(KeyStore.Cursors, ListField, action((field: ListField) => { + let cursors = field.Data; + if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) { + cursors[ind].Data[1] = position; + } else { + let entry = new TupleField<[string, string], [number, number]>([textInfo, position]); + cursors.push(entry); + } + })); + }); + } } - } - @undoBatch - @action - protected drop(e: Event, de: DragManager.DropEvent): boolean { - if (de.data instanceof DragManager.DocumentDragData) { - if (de.data.aliasOnDrop || de.data.copyOnDrop) { - [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key => - de.data.draggedDocuments.map((draggedDocument: Document, i: number) => - draggedDocument.GetTAsync(key, NumberField, (f: Opt) => f ? de.data.droppedDocuments[i].SetNumber(key, f.Data) : null))); - } - let added = false; - if (de.data.aliasOnDrop || de.data.copyOnDrop) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); - } else if (de.data.moveDocument) { - const move = de.data.moveDocument; - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = move(d, this.props.Document, this.props.addDocument); - return moved || added; - }, false); - } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); + @undoBatch + @action + protected drop(e: Event, de: DragManager.DropEvent): boolean { + if (de.data instanceof DragManager.DocumentDragData) { + if (de.data.aliasOnDrop || de.data.copyOnDrop) { + [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key => + de.data.draggedDocuments.map((draggedDocument: Document, i: number) => + draggedDocument.GetTAsync(key, NumberField, (f: Opt) => f ? de.data.droppedDocuments[i].SetNumber(key, f.Data) : null))); + } + let added = false; + if (de.data.aliasOnDrop || de.data.copyOnDrop) { + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = this.props.addDocument(d); + return moved || added; + }, false); + } else if (de.data.moveDocument) { + const move = de.data.moveDocument; + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = move(d, this.props.Document, this.props.addDocument); + return moved || added; + }, false); + } else { + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = this.props.addDocument(d); + return moved || added; + }, false); + } + e.stopPropagation(); + return added; } - e.stopPropagation(); - return added; + return false; } - return false; - } - protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { - let ctor: ((path: string, options: DocumentOptions) => (Document | Promise)) | undefined = undefined; - if (type.indexOf("image") !== -1) { - ctor = Documents.ImageDocument; - } - if (type.indexOf("video") !== -1) { - ctor = Documents.VideoDocument; - } - if (type.indexOf("audio") !== -1) { - ctor = Documents.AudioDocument; - } - if (type.indexOf("pdf") !== -1) { - ctor = Documents.PdfDocument; - options.nativeWidth = 1200; - } - if (type.indexOf("excel") !== -1) { - ctor = Documents.DBDocument; - options.copyDraggedItems = true; - } - if (type.indexOf("html") !== -1) { - if (path.includes('localhost')) { - let s = path.split('/'); - let id = s[s.length - 1]; - Server.GetField(id).then(field => { - if (field instanceof Document) { - let alias = field.CreateAlias(); - alias.SetNumber(KeyStore.X, options.x || 0); - alias.SetNumber(KeyStore.Y, options.y || 0); - alias.SetNumber(KeyStore.Width, options.width || 300); - alias.SetNumber(KeyStore.Height, options.height || options.width || 300); - this.props.addDocument(alias, false); - } - }); - return undefined; + protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { + let ctor: ((path: string, options: DocumentOptions) => (Document | Promise)) | undefined = undefined; + if (type.indexOf("image") !== -1) { + ctor = Documents.ImageDocument; + } + if (type.indexOf("video") !== -1) { + ctor = Documents.VideoDocument; + } + if (type.indexOf("audio") !== -1) { + ctor = Documents.AudioDocument; + } + if (type.indexOf("pdf") !== -1) { + ctor = Documents.PdfDocument; + options.nativeWidth = 1200; + } + if (type.indexOf("excel") !== -1) { + ctor = Documents.DBDocument; + options.copyDraggedItems = true; } - ctor = Documents.WebDocument; - options = { height: options.width, ...options, title: path }; + if (type.indexOf("html") !== -1) { + if (path.includes('localhost')) { + let s = path.split('/'); + let id = s[s.length - 1]; + Server.GetField(id).then(field => { + if (field instanceof Document) { + let alias = field.CreateAlias(); + alias.SetNumber(KeyStore.X, options.x || 0); + alias.SetNumber(KeyStore.Y, options.y || 0); + alias.SetNumber(KeyStore.Width, options.width || 300); + alias.SetNumber(KeyStore.Height, options.height || options.width || 300); + this.props.addDocument(alias, false); + } + }); + return undefined; + } + ctor = Documents.WebDocument; + options = { height: options.width, ...options, title: path }; + } + return ctor ? ctor(path, options) : undefined; } - return ctor ? ctor(path, options) : undefined; - } - @undoBatch - @action - protected onDrop(e: React.DragEvent, options: DocumentOptions): void { - let html = e.dataTransfer.getData("text/html"); - let text = e.dataTransfer.getData("text/plain"); + @undoBatch + @action + protected onDrop(e: React.DragEvent, options: DocumentOptions): void { + let html = e.dataTransfer.getData("text/html"); + let text = e.dataTransfer.getData("text/plain"); - if (text && text.startsWith("[] = []; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < e.dataTransfer.items.length; i++) { - const upload = window.location.origin + RouteStore.upload; - let item = e.dataTransfer.items[i]; - if (item.kind === "string" && item.type.indexOf("uri") !== -1) { - let str: string; - let prom = new Promise(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s))))) - .then(result => { - let type = result.headers["content-type"]; - if (type) { - this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) - .then(doc => doc && this.props.addDocument(doc, false)); - } - }); - promises.push(prom); + if (html && html.indexOf("[] = []; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < e.dataTransfer.items.length; i++) { + const upload = window.location.origin + RouteStore.upload; + let item = e.dataTransfer.items[i]; + if (item.kind === "string" && item.type.indexOf("uri") !== -1) { + let str: string; + let prom = new Promise(resolve => e.dataTransfer.items[i].getAsString(resolve)) + .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s))))) + .then(result => { + let type = result.headers["content-type"]; + if (type) { + this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) + .then(doc => doc && this.props.addDocument(doc, false)); + } + }); + promises.push(prom); } - let dropFileName = file ? file.name : "-empty-"; - - let prom = fetch(upload, { - method: 'POST', - body: formData - }).then(async (res: Response) => { - (await res.json()).map(action((file: any) => { - let path = window.location.origin + file; - let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName }); - - docPromise.then(action((doc?: Document) => { - let docs = this.props.Document.GetT(KeyStore.Data, ListField); - if (docs !== FieldWaiting) { - if (!docs) { - docs = new ListField(); - this.props.Document.Set(KeyStore.Data, docs); - } - if (doc) { - docs.Data.push(doc); + let type = item.type; + if (item.kind === "file") { + let file = item.getAsFile(); + let formData = new FormData(); + + if (file) { + formData.append('file', file); + } + let dropFileName = file ? file.name : "-empty-"; + + let prom = fetch(upload, { + method: 'POST', + body: formData + }).then(async (res: Response) => { + (await res.json()).map(action((file: any) => { + let path = window.location.origin + file; + let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName }); + + docPromise.then(action((doc?: Document) => { + let docs = this.props.Document.GetT(KeyStore.Data, ListField); + if (docs !== FieldWaiting) { + if (!docs) { + docs = new ListField(); + this.props.Document.Set(KeyStore.Data, docs); + } + if (doc) { + docs.Data.push(doc); + } } - } + })); })); - })); - }); - promises.push(prom); + }); + promises.push(prom); + } } - } - if (promises.length) { - Promise.all(promises).finally(() => batch.end()); - } else { - batch.end(); + if (promises.length) { + Promise.all(promises).finally(() => batch.end()); + } else { + batch.end(); + } } } + return CollectionSubView; } + diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 945c01059..32104b49d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,5 @@ import { action, computed, observable, trace } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../../fields/Document"; -import { KeyStore } from "../../../../fields/KeyStore"; import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; @@ -13,7 +11,7 @@ import { InkingCanvas } from "../../InkingCanvas"; import { MainOverlayTextBox } from "../../MainOverlayTextBox"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; @@ -21,17 +19,28 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); -import { BooleanField } from "../../../../fields/BooleanField"; +import { createSchema, makeInterface } from "../../../../new_fields/Schema"; +import { Doc } from "../../../../new_fields/Doc"; +import { FieldValue } from "../../../../new_fields/Types"; + +export const panZoomSchema = createSchema({ + panX: "number", + panY: "number", + zoom: "number" +}); + +type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema]>; +const PanZoomDocument: (doc: Doc) => PanZoomDocument = makeInterface(panZoomSchema, positionSchema); @observer -export class CollectionFreeFormView extends CollectionSubView { +export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) private _lastX: number = 0; private _lastY: number = 0; private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index d8de5afec..845213366 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -31,13 +31,13 @@ export interface FieldViewProps { select: (isCtrlPressed: boolean) => void; isTopMost: boolean; selectOnLoad: boolean; - addDocument?: (document: Document, allowDuplicates?: boolean) => boolean; - removeDocument?: (document: Document) => boolean; - moveDocument?: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; + addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument?: (document: Doc) => boolean; + moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - focus: (doc: Document) => void; + focus: (doc: Doc) => void; } @observer diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index 1d1f56844..6022716be 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -1,9 +1,10 @@ import { Interface, ToInterface, Cast, FieldCtor, ToConstructor, HasTail, Head, Tail } from "./Types"; -import { Doc } from "./Doc"; +import { Doc, Field, ObjectField } from "./Doc"; +import { URLField } from "./URLField"; -type All = { - 1: makeInterface, U> & All, U>, - 0: makeInterface, U> +type All = { + 1: makeInterface<[Head], U> & All, U>, + 0: makeInterface<[Head], U> }[HasTail extends true ? 1 : 0]; type AllToInterface = { @@ -11,10 +12,14 @@ type AllToInterface = { 0: ToInterface> }[HasTail extends true ? 1 : 0]; +export const emptySchema = createSchema({}); +export const Document = makeInterface(emptySchema); +export type Document = makeInterface<[typeof emptySchema]>; + export type makeInterface = Partial> & U; // export function makeInterface(schemas: T): (doc: U) => All; // export function makeInterface(schema: T): (doc: U) => makeInterface; -export function makeInterface(schemas: T): (doc: U) => All { +export function makeInterface(...schemas: T): (doc: U) => makeInterface { let schema: Interface = {}; for (const s of schemas) { for (const key in s) { diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 0392dc2fb..e4d15e276 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -11,7 +11,7 @@ export type ToType = export type ToConstructor = T extends string ? "string" : T extends number ? "number" : - T extends boolean ? "boolean" : { new(...args: any[]): T }; + T extends boolean ? "boolean" : new (...args: any[]) => T; export type ToInterface = { [P in keyof T]: ToType; -- cgit v1.2.3-70-g09d2 From e5171e5fbc73c93487c1545871e52e6b8fa190a6 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sun, 21 Apr 2019 18:15:24 -0400 Subject: Refactored schema to be faster hopefully --- src/new_fields/Schema.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index 6022716be..696a5d2a8 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -16,6 +16,7 @@ export const emptySchema = createSchema({}); export const Document = makeInterface(emptySchema); export type Document = makeInterface<[typeof emptySchema]>; +const DocSymbol = Symbol("Doc"); export type makeInterface = Partial> & U; // export function makeInterface(schemas: T): (doc: U) => All; // export function makeInterface(schema: T): (doc: U) => makeInterface; @@ -26,16 +27,29 @@ export function makeInterface(...schemas: schema[key] = s[key]; } } - return function (doc: any) { - return new Proxy(doc, { - get(target, prop) { - const field = target[prop]; - if (prop in schema) { - return Cast(field, (schema as any)[prop]); - } - return field; + const proto = new Proxy({}, { + get(target: any, prop) { + if (prop === DocSymbol) { + return target[prop]; } - }); + const field = target[DocSymbol][prop]; + if (prop in schema) { + return Cast(field, (schema as any)[prop]); + } + return field; + }, + set(target: any, prop, value) { + if (prop === DocSymbol) { + target[prop] = value; + } + target[DocSymbol][prop] = value; + return true; + } + }); + return function (doc: any) { + const obj = Object.create(proto); + obj.__doc = doc; + return obj; }; } -- cgit v1.2.3-70-g09d2 From fd5c4f18d0af6571508f142ca400e6d752f19800 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sun, 21 Apr 2019 18:41:39 -0400 Subject: Changed WebBox --- src/client/views/nodes/WebBox.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 1edb4d826..e3b6e1c05 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,11 +1,10 @@ import "./WebBox.scss"; import React = require("react"); -import { WebField } from '../../../fields/WebField'; import { FieldViewProps, FieldView } from './FieldView'; -import { FieldWaiting } from '../../../fields/Field'; import { observer } from "mobx-react"; import { computed } from 'mobx'; -import { KeyStore } from '../../../fields/KeyStore'; +import { HtmlField } from "../../../new_fields/HtmlField"; +import { URLField } from "../../../new_fields/URLField"; @observer export class WebBox extends React.Component { @@ -16,8 +15,6 @@ export class WebBox extends React.Component { super(props); } - @computed get html(): string { return this.props.Document.GetHtml(KeyStore.Data, ""); } - _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; @@ -36,14 +33,18 @@ export class WebBox extends React.Component { } } render() { - let field = this.props.Document.Get(this.props.fieldKey); - let path = field === FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : - field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; - + let field = this.props.Document[this.props.fieldKey]; + let view; + if (field instanceof HtmlField) { + view = + } else if (field instanceof URLField) { + view =