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/fields/NewDoc.ts | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/fields/NewDoc.ts (limited to 'src/fields') 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/fields') 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/fields') 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/fields') 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/fields') 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/fields') 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/fields') 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/fields') 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 50a950238def89ae30d386676bbfe884c8b8c7be Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 24 Apr 2019 02:23:36 -0400 Subject: Added get fields and did some more fixing --- src/client/DocServer.ts | 25 +++ src/client/documents/Documents.ts | 43 ++-- src/fields/Document.ts | 430 -------------------------------------- src/fields/IconFIeld.ts | 25 --- src/fields/InkField.ts | 53 ----- src/new_fields/IconField.ts | 14 ++ src/new_fields/InkField.ts | 31 +++ src/server/Message.ts | 1 + src/server/index.ts | 6 +- 9 files changed, 101 insertions(+), 527 deletions(-) delete mode 100644 src/fields/Document.ts delete mode 100644 src/fields/IconFIeld.ts delete mode 100644 src/fields/InkField.ts create mode 100644 src/new_fields/IconField.ts create mode 100644 src/new_fields/InkField.ts (limited to 'src/fields') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 615e48af0..cba0c4770 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -30,6 +30,31 @@ export namespace DocServer { } } + export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt }> { + const requestedIds: string[] = []; + const waitingIds: string[] = []; + const promises: Promise>[] = []; + const map: { [id: string]: Opt } = {}; + for (const id of ids) { + const cached = _cache[id]; + if (cached === undefined) { + requestedIds.push(id); + } else if (cached instanceof Promise) { + promises.push(cached); + waitingIds.push(id); + } else { + map[id] = cached; + } + } + const prom = Utils.EmitCallback(_socket, MessageStore.GetFields, requestedIds); + requestedIds.map((id, index) => _cache[id] = prom.then((fields: RefField[]) => fields[index])); + const fields = await prom; + requestedIds.map((id, index) => map[id] = fields[index]); + const otherFields = await Promise.all(promises); + waitingIds.map((id, index) => map[id] = otherFields[index]); + return map; + } + export function UpdateField(id: string, diff: any) { Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a145a76c9..a5ce7c076 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -26,6 +26,9 @@ import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../ne import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; import { Cast } from "../../new_fields/Types"; +import { IconField } from "../../new_fields/IconField"; +import { listSpec } from "../../new_fields/Schema"; +import { DocServer } from "../DocServer"; export interface DocumentOptions { x?: number; @@ -73,17 +76,17 @@ export namespace Docs { const iconProtoId = "iconProto"; export function initProtos(): Promise { - return Server.GetFields([textProtoId, histoProtoId, collProtoId, pdfProtoId, imageProtoId, videoProtoId, audioProtoId, webProtoId, kvpProtoId]).then(fields => { - textProto = fields[textProtoId] as Document || CreateTextPrototype(); - histoProto = fields[histoProtoId] as Document || CreateHistogramPrototype(); - collProto = fields[collProtoId] as Document || CreateCollectionPrototype(); - imageProto = fields[imageProtoId] as Document || CreateImagePrototype(); - webProto = fields[webProtoId] as Document || CreateWebPrototype(); - kvpProto = fields[kvpProtoId] as Document || CreateKVPPrototype(); - videoProto = fields[videoProtoId] as Document || CreateVideoPrototype(); - audioProto = fields[audioProtoId] as Document || CreateAudioPrototype(); - pdfProto = fields[pdfProtoId] as Document || CreatePdfPrototype(); - iconProto = fields[iconProtoId] as Document || CreateIconPrototype(); + return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, pdfProtoId, imageProtoId, videoProtoId, audioProtoId, webProtoId, kvpProtoId]).then(fields => { + textProto = fields[textProtoId] as Doc || CreateTextPrototype(); + histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype(); + collProto = fields[collProtoId] as Doc || CreateCollectionPrototype(); + imageProto = fields[imageProtoId] as Doc || CreateImagePrototype(); + webProto = fields[webProtoId] as Doc || CreateWebPrototype(); + kvpProto = fields[kvpProtoId] as Doc || CreateKVPPrototype(); + videoProto = fields[videoProtoId] as Doc || CreateVideoPrototype(); + audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype(); + pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype(); + iconProto = fields[iconProtoId] as Doc || CreateIconPrototype(); }); } @@ -191,25 +194,29 @@ export namespace Docs { let ctlog = await Gateway.Instance.GetSchema(url, schemaName); if (ctlog && ctlog.schemas) { let schema = ctlog.schemas[0]; - let schemaDoc = Documents.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); - let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]); + let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); + let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc)); + if (!schemaDocuments) { + return; + } + const docs = schemaDocuments; CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { - Server.GetField(attr.displayName! + ".alias", action((field: Opt) => { - if (field instanceof Document) { - schemaDocuments.push(field); + DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt) => { + if (field instanceof Doc) { + docs.push(field); } else { var atmod = new ColumnAttributeModel(attr); let histoOp = new HistogramOperation(schema.displayName!, new AttributeTransformationModel(atmod, AggregateFunction.None), new AttributeTransformationModel(atmod, AggregateFunction.Count), new AttributeTransformationModel(atmod, AggregateFunction.Count)); - schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias")); + docs.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias")); } })); }); return schemaDoc; } - return Documents.TreeDocument([], { width: 50, height: 100, title: schemaName }); + return Docs.TreeDocument([], { width: 50, height: 100, title: schemaName }); } export function WebDocument(url: string, options: DocumentOptions = {}) { return CreateInstance(webProto, new WebField(new URL(url)), options); diff --git a/src/fields/Document.ts b/src/fields/Document.ts deleted file mode 100644 index 7cf784f0e..000000000 --- a/src/fields/Document.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { Key } from "./Key"; -import { KeyStore } from "./KeyStore"; -import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field"; -import { NumberField } from "./NumberField"; -import { ObservableMap, computed, action, runInAction } from "mobx"; -import { TextField } from "./TextField"; -import { ListField } from "./ListField"; -import { Server } from "../client/Server"; -import { Types } from "../server/Message"; -import { UndoManager } from "../client/util/UndoManager"; -import { HtmlField } from "./HtmlField"; -import { BooleanField } from "./BooleanField"; -import { allLimit } from "async"; -import { prototype } from "nodemailer/lib/smtp-pool"; -import { HistogramField } from "../client/northstar/dash-fields/HistogramField"; - -export class Document extends Field { - //TODO tfs: We should probably store FieldWaiting in fields when we request it from the server so that we don't set up multiple server gets for the same document and field - public fields: ObservableMap = new ObservableMap(); - public _proxies: ObservableMap = new ObservableMap(); - - constructor(id?: string, save: boolean = true) { - super(id); - - if (save) { - Server.UpdateField(this); - } - } - static FromJson(data: any, id: string, save: boolean): Document { - let doc = new Document(id, save); - let fields = data as [string, string][]; - fields.forEach(pair => doc._proxies.set(pair[0], pair[1])); - return doc; - } - - UpdateFromServer(data: [string, string][]) { - for (const key in data) { - const element = data[key]; - this._proxies.set(element[0], element[1]); - } - } - - public Width = () => this.GetNumber(KeyStore.Width, 0); - public Height = () => this.GetNumber(KeyStore.Height, this.GetNumber(KeyStore.NativeWidth, 0) ? (this.GetNumber(KeyStore.NativeHeight, 0) / this.GetNumber(KeyStore.NativeWidth, 0)) * this.GetNumber(KeyStore.Width, 0) : 0); - public Scale = () => this.GetNumber(KeyStore.Scale, 1); - - @computed - public get Title(): string { - let title = this.Get(KeyStore.Title, true); - if (title || title === FieldWaiting) { - if (title !== FieldWaiting && title instanceof TextField) { - return title.Data; - } - else return "-waiting-"; - } - let parTitle = this.GetT(KeyStore.Title, TextField); - if (parTitle || parTitle === FieldWaiting) { - if (parTitle !== FieldWaiting) return parTitle.Data + ".alias"; - else return "-waiting-.alias"; - } - return "-untitled-"; - } - - @computed - public get Fields() { - return this.fields; - } - - /** - * Get the field in the document associated with the given key. If the - * associated field has not yet been filled in from the server, a request - * to the server will automatically be sent, the value will be filled in - * when the request is completed, and {@link Field.ts#FieldWaiting} will be returned. - * @param key - The key of the value to get - * @param ignoreProto - If true, ignore any prototype this document - * might have and only search for the value on this immediate document. - * If false (default), search up the prototype chain, starting at this document, - * for a document that has a field associated with the given key, and return the first - * one found. - * - * @returns If the document does not have a field associated with the given key, returns `undefined`. - * If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}. - * If the document does have an associated field, and the field has not been fetched from the server, returns the associated field. - */ - Get(key: Key, ignoreProto: boolean = false): FieldValue { - let field: FieldValue; - if (ignoreProto) { - if (this.fields.has(key.Id)) { - field = this.fields.get(key.Id)!.field; - } else if (this._proxies.has(key.Id)) { - Server.GetDocumentField(this, key); - /* - The field might have been instantly filled from the cache - Maybe we want to just switch back to returning the value - from Server.GetDocumentField if it's in the cache - */ - if (this.fields.has(key.Id)) { - field = this.fields.get(key.Id)!.field; - } else { - field = FieldWaiting; - } - } - } else { - let doc: FieldValue = this; - while (doc && field !== FieldWaiting) { - let curField = doc.fields.get(key.Id); - let curProxy = doc._proxies.get(key.Id); - if (!curField || (curProxy && curField.field.Id !== curProxy)) { - if (curProxy) { - Server.GetDocumentField(doc, key); - /* - The field might have been instantly filled from the cache - Maybe we want to just switch back to returning the value - from Server.GetDocumentField if it's in the cache - */ - if (this.fields.has(key.Id)) { - field = this.fields.get(key.Id)!.field; - } else { - field = FieldWaiting; - } - break; - } - if ( - doc.fields.has(KeyStore.Prototype.Id) || - doc._proxies.has(KeyStore.Prototype.Id) - ) { - doc = doc.GetPrototype(); - } else { - break; - } - } else { - field = curField.field; - break; - } - } - if (doc === FieldWaiting) field = FieldWaiting; - } - - return field; - } - - /** - * Tries to get the field associated with the given key, and if there is an - * associated field, calls the given callback with that field. - * @param key - The key of the value to get - * @param callback - A function that will be called with the associated field, if it exists, - * once it is fetched from the server (this may be immediately if the field has already been fetched). - * Note: The callback will not be called if there is no associated field. - * @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise - */ - GetAsync(key: Key, callback: (field: Opt) => void): void { - //TODO: This currently doesn't deal with prototypes - let field = this.fields.get(key.Id); - if (field && field.field) { - callback(field.field); - } else if (this._proxies.has(key.Id)) { - Server.GetDocumentField(this, key, callback); - } else if (this._proxies.has(KeyStore.Prototype.Id)) { - this.GetTAsync(KeyStore.Prototype, Document, proto => { - if (proto) { - proto.GetAsync(key, callback); - } else { - callback(undefined); - } - }); - } else { - callback(undefined); - } - } - - GetTAsync(key: Key, ctor: { new(): T }): Promise>; - GetTAsync( - key: Key, - ctor: { new(): T }, - callback: (field: Opt) => void - ): void; - GetTAsync( - key: Key, - ctor: { new(): T }, - callback?: (field: Opt) => void - ): Promise> | void { - let fn = (cb: (field: Opt) => void) => { - return this.GetAsync(key, field => { - cb(Cast(field, ctor)); - }); - }; - if (callback) { - fn(callback); - } else { - return new Promise(fn); - } - } - - /** - * Same as {@link Document#GetAsync}, except a field of the given type - * will be created if there is no field associated with the given key, - * or the field associated with the given key is not of the given type. - * @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc. - */ - GetOrCreateAsync( - key: Key, - ctor: { new(): T }, - callback: (field: T) => void - ): void { - //This currently doesn't deal with prototypes - if (this._proxies.has(key.Id)) { - Server.GetDocumentField(this, key, field => { - if (field && field instanceof ctor) { - callback(field); - } else { - let newField = new ctor(); - this.Set(key, newField); - callback(newField); - } - }); - } else { - let newField = new ctor(); - this.Set(key, newField); - callback(newField); - } - } - - /** - * Same as {@link Document#Get}, except that it will additionally - * check if the field is of the given type. - * @param ctor - Constructor of the field type to get. E.g., `TextField`, `ImageField`, etc. - * @returns Same as {@link Document#Get}, except will return `undefined` - * if there is an associated field but it is of the wrong type. - */ - GetT( - key: Key, - ctor: { new(...args: any[]): T }, - ignoreProto: boolean = false - ): FieldValue { - var getfield = this.Get(key, ignoreProto); - if (getfield !== FieldWaiting) { - return Cast(getfield, ctor); - } - return FieldWaiting; - } - - GetOrCreate( - key: Key, - ctor: { new(): T }, - ignoreProto: boolean = false - ): T { - const field = this.GetT(key, ctor, ignoreProto); - if (field && field !== FieldWaiting) { - return field; - } - const newField = new ctor(); - this.Set(key, newField); - return newField; - } - - GetData( - key: Key, - ctor: { new(): U }, - defaultVal: T - ): T { - let val = this.Get(key); - let vval = val && val instanceof ctor ? val.Data : defaultVal; - return vval; - } - - GetHtml(key: Key, defaultVal: string): string { - return this.GetData(key, HtmlField, defaultVal); - } - - GetBoolean(key: Key, defaultVal: boolean): boolean { - return this.GetData(key, BooleanField, defaultVal); - } - - GetNumber(key: Key, defaultVal: number): number { - return this.GetData(key, NumberField, defaultVal); - } - - GetText(key: Key, defaultVal: string): string { - return this.GetData(key, TextField, defaultVal); - } - - GetList(key: Key, defaultVal: T[]): T[] { - return this.GetData>(key, ListField, defaultVal); - } - - @action - Set(key: Key, field: Field | undefined, setOnPrototype = false): void { - let old = this.fields.get(key.Id); - let oldField = old ? old.field : undefined; - if (setOnPrototype) { - this.SetOnPrototype(key, field); - } else { - if (field) { - this.fields.set(key.Id, { key, field }); - this._proxies.set(key.Id, field.Id); - // Server.AddDocumentField(this, key, field); - } else { - this.fields.delete(key.Id); - this._proxies.delete(key.Id); - // Server.DeleteDocumentField(this, key); - } - Server.UpdateField(this); - } - if (oldField || field) { - UndoManager.AddEvent({ - undo: () => this.Set(key, oldField, setOnPrototype), - redo: () => this.Set(key, field, setOnPrototype) - }); - } - } - - @action - SetOnPrototype(key: Key, field: Field | undefined): void { - this.GetTAsync(KeyStore.Prototype, Document, (f: Opt) => { - f && f.Set(key, field); - }); - } - - @action - SetDataOnPrototype(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) { - this.GetTAsync(KeyStore.Prototype, Document, (f: Opt) => { - f && f.SetData(key, value, ctor, replaceWrongType); - }); - } - - @action - SetData(key: Key, value: T, ctor: { new(data: T): U }, replaceWrongType = true) { - let field = this.Get(key, true); - if (field instanceof ctor) { - field.Data = value; - } else if (!field || replaceWrongType) { - let newField = new ctor(value); - // newField.Data = value; - this.Set(key, newField); - } - } - - @action - SetText(key: Key, value: string, replaceWrongType = true) { - this.SetData(key, value, TextField, replaceWrongType); - } - @action - SetBoolean(key: Key, value: boolean, replaceWrongType = true) { - this.SetData(key, value, BooleanField, replaceWrongType); - } - @action - SetNumber(key: Key, value: number, replaceWrongType = true) { - this.SetData(key, value, NumberField, replaceWrongType); - } - - GetPrototype(): FieldValue { - return this.GetT(KeyStore.Prototype, Document, true); - } - - GetAllPrototypes(): Document[] { - let protos: Document[] = []; - let doc: FieldValue = this; - while (doc && doc !== FieldWaiting) { - protos.push(doc); - doc = doc.GetPrototype(); - } - return protos; - } - - CreateAlias(id?: string): Document { - let alias = new Document(id); - this.GetTAsync(KeyStore.Prototype, Document, (f: Opt) => { - f && alias.Set(KeyStore.Prototype, f); - }); - - return alias; - } - - MakeDelegate(id?: string): Document { - let delegate = new Document(id); - - delegate.Set(KeyStore.Prototype, this); - - return delegate; - } - - ToScriptString(): string { - return ""; - } - - TrySetValue(value: any): boolean { - throw new Error("Method not implemented."); - } - GetValue() { - return this.Title; - var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")"; - return title; - //throw new Error("Method not implemented."); - } - Copy(copyProto?: boolean, id?: string): Field { - let copy = new Document(); - this._proxies.forEach((fieldid, keyid) => { // copy each prototype field - let key = KeyStore.KeyLookup(keyid); - if (key) { - this.GetAsync(key, (field: Opt) => { - if (key === KeyStore.Prototype && copyProto) { // handle prototype field specially - if (field instanceof Document) { - copy.Set(key, field.Copy(false)); // only copying one level of prototypes for now... - } - } - else - if (field instanceof Document) { // ... TODO bcz: should we copy documents or reference them - copy.Set(key!, field); - } - else if (field) { - copy.Set(key!, field.Copy()); - } - }); - } - }); - return copy; - } - - ToJson() { - let fields: [string, string][] = []; - this._proxies.forEach((field, key) => - field && fields.push([key, field])); - - return { - type: Types.Document, - data: fields, - id: this.Id - }; - } -} diff --git a/src/fields/IconFIeld.ts b/src/fields/IconFIeld.ts deleted file mode 100644 index a6694cc49..000000000 --- a/src/fields/IconFIeld.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BasicField } from "./BasicField"; -import { FieldId } from "./Field"; -import { Types } from "../server/Message"; - -export class IconField extends BasicField { - constructor(data: string = "", id?: FieldId, save: boolean = true) { - super(data, save, id); - } - - ToScriptString(): string { - return `new IconField("${this.Data}")`; - } - - Copy() { - return new IconField(this.Data); - } - - ToJson() { - return { - type: Types.Icon, - data: this.Data, - id: this.Id - }; - } -} \ No newline at end of file diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts deleted file mode 100644 index 2eacd7d0c..000000000 --- a/src/fields/InkField.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Types } from "../server/Message"; -import { FieldId } from "./Field"; -import { observable, ObservableMap } from "mobx"; - -export enum InkTool { - None, - Pen, - Highlighter, - Eraser -} -export interface StrokeData { - pathData: Array<{ x: number, y: number }>; - color: string; - width: string; - tool: InkTool; - page: number; -} -export type StrokeMap = Map; - -export class InkField extends BasicField { - constructor(data: StrokeMap = new Map, id?: FieldId, save: boolean = true) { - super(data, save, id); - } - - ToScriptString(): string { - return `new InkField("${this.Data}")`; - } - - Copy() { - return new InkField(this.Data); - } - - ToJson() { - return { - type: Types.Ink, - data: this.Data, - id: this.Id, - }; - } - - UpdateFromServer(data: any) { - this.data = new ObservableMap(data); - } - - static FromJson(id: string, data: any): InkField { - let map: StrokeMap = new Map(); - Object.keys(data).forEach(key => { - map.set(key, data[key]); - }); - return new InkField(map, id, false); - } -} \ No newline at end of file diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts new file mode 100644 index 000000000..32f3aa4d5 --- /dev/null +++ b/src/new_fields/IconField.ts @@ -0,0 +1,14 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { serializable, primitive } from "serializr"; +import { ObjectField } from "./Doc"; + +@Deserializable("icon") +export class IconField extends ObjectField { + @serializable(primitive()) + readonly layout: string; + + constructor(layout: string) { + super(); + this.layout = layout; + } +} diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts new file mode 100644 index 000000000..cdb34cedf --- /dev/null +++ b/src/new_fields/InkField.ts @@ -0,0 +1,31 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; +import { ObjectField } from "./Doc"; + +export enum InkTool { + None, + Pen, + Highlighter, + Eraser +} +export interface StrokeData { + pathData: Array<{ x: number, y: number }>; + color: string; + width: string; + tool: InkTool; + page: number; +} + +const pointSchema = createSimpleSchema({ + x: true, y: true +}); + +const strokeDataSchema = createSimpleSchema({ + pathData: list(object(pointSchema)), + "*": true +}); + +export class InkField extends ObjectField { + @serializable(map(object(strokeDataSchema))) + readonly inkData: Map = new Map; +} diff --git a/src/server/Message.ts b/src/server/Message.ts index b01934724..81da44f72 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -42,6 +42,7 @@ export namespace MessageStore { export const DeleteAll = new Message("Delete All"); export const GetRefField = new Message("Get Ref Field"); + export const GetRefFields = new Message("Get Ref Fields"); export const UpdateField = new Message("Update Ref Field"); export const CreateField = new Message("Create Ref Field"); } diff --git a/src/server/index.ts b/src/server/index.ts index d6d5f0e55..10158eb96 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -16,7 +16,6 @@ import { Socket } from 'socket.io'; import * as webpack from 'webpack'; import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; -import { Field, FieldId } from '../fields/Field'; import { Utils } from '../Utils'; import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller'; import { DashUserModel } from './authentication/models/user_model'; @@ -236,6 +235,7 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); Utils.AddServerHandler(socket, MessageStore.GetRefField, GetRefField); + Utils.AddServerHandler(socket, MessageStore.GetRefFields, GetRefFields); }); function deleteFields() { @@ -270,6 +270,10 @@ function GetRefField([id, callback]: [string, (result?: Transferable) => void]) Database.Instance.getDocument(id, callback, "newDocuments"); } +function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { + Database.Instance.getDocuments(ids, callback, "newDocuments"); +} + function UpdateField(socket: Socket, diff: Diff) { Database.Instance.update(diff.id, diff.diff, () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); -- cgit v1.2.3-70-g09d2 From 7cda7f95e724bb621c57b5c53b083e6d6245afa5 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 26 Apr 2019 21:14:50 -0400 Subject: More --- src/client/views/InkingCanvas.tsx | 29 +- src/client/views/InkingControl.tsx | 7 +- .../views/collections/CollectionTreeView.tsx | 51 ++- src/client/views/nodes/LinkEditor.tsx | 16 +- src/debug/Viewer.tsx | 368 ++++++++++----------- src/fields/ScriptField.ts | 174 +++++----- src/mobile/ImageUpload.tsx | 34 +- src/new_fields/Doc.ts | 4 +- .../authentication/controllers/WorkspacesMenu.tsx | 16 +- 9 files changed, 344 insertions(+), 355 deletions(-) (limited to 'src/fields') diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 47ee8eb85..1e26893c5 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -1,9 +1,5 @@ import { action, computed, trace, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../fields/Document"; -import { FieldWaiting } from "../../fields/Field"; -import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField"; -import { KeyStore } from "../../fields/KeyStore"; import { Utils } from "../../Utils"; import { Transform } from "../util/Transform"; import "./InkingCanvas.scss"; @@ -11,10 +7,13 @@ import { InkingControl } from "./InkingControl"; import { InkingStroke } from "./InkingStroke"; import React = require("react"); import { undoBatch, UndoManager } from "../util/UndoManager"; +import { StrokeData, InkField, InkTool } from "../../new_fields/InkField"; +import { Doc } from "../../new_fields/Doc"; +import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; interface InkCanvasProps { getScreenTransform: () => Transform; - Document: Document; + Document: Doc; children: () => JSX.Element[]; } @@ -23,7 +22,7 @@ export class InkingCanvas extends React.Component { maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome @observable inkMidX: number = 0; @observable inkMidY: number = 0; - private previousState?: StrokeMap; + private previousState?: Map; private _currentStrokeId: string = ""; public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean { return stroke.pathData.reduce((inside: boolean, val) => inside || @@ -33,9 +32,9 @@ export class InkingCanvas extends React.Component { } componentDidMount() { - this.props.Document.GetTAsync(KeyStore.Ink, InkField, ink => runInAction(() => { + PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => { if (ink) { - let bounds = Array.from(ink.Data).reduce(([mix, max, miy, may], [id, strokeData]) => + let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) => strokeData.pathData.reduce(([mix, max, miy, may], p) => [Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)], [mix, max, miy, may]), @@ -47,13 +46,13 @@ export class InkingCanvas extends React.Component { } @computed - get inkData(): StrokeMap { - let map = this.props.Document.GetT(KeyStore.Ink, InkField); - return !map || map === FieldWaiting ? new Map : new Map(map.Data); + get inkData(): Map { + let map = Cast(this.props.Document.ink, InkField); + return !map ? new Map : new Map(map.inkData); } - set inkData(value: StrokeMap) { - this.props.Document.SetDataOnPrototype(KeyStore.Ink, value, InkField); + set inkData(value: Map) { + Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value)); } @action @@ -78,7 +77,7 @@ export class InkingCanvas extends React.Component { color: InkingControl.Instance.selectedColor, width: InkingControl.Instance.selectedWidth, tool: InkingControl.Instance.selectedTool, - page: this.props.Document.GetNumber(KeyStore.CurPage, -1) + page: NumCast(this.props.Document.curPage, -1) }); this.inkData = data; } @@ -137,7 +136,7 @@ export class InkingCanvas extends React.Component { @computed get drawnPaths() { - let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); + let curPage = NumCast(this.props.Document.curPage, -1); let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => { if (strokeData.page === -1 || strokeData.page === curPage) { paths.push( void; + document: Doc; + deleteDoc: (doc: Doc) => void; moveDocument: DragManager.MoveFunction; copyOnDrag: boolean; } @@ -43,9 +42,9 @@ class TreeView extends React.Component { @action remove = (document: Document) => { - var children = this.props.document.GetT>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + var children = Cast(this.props.document.data, listSpec(Doc)); + if (children) { + children.splice(children.indexOf(document), 1); } } @@ -80,15 +79,15 @@ class TreeView extends React.Component { display={"inline"} contents={titleString} height={36} - GetValue={() => this.props.document.Title} + GetValue={() => StrCast(this.props.document.title)} SetValue={(value: string) => { - this.props.document.SetText(KeyStore.Title, value); + this.props.document.title = value; return true; }} />); return (
- {editableView(this.props.document.Title)} + {editableView(StrCast(this.props.document.title))}
); } @@ -96,12 +95,12 @@ class TreeView extends React.Component { render() { let bulletType = BulletType.List; let childElements: JSX.Element | undefined = undefined; - var children = this.props.document.GetT>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { // add children for a collection + var children = Cast(this.props.document.data, listSpec(Doc)); + if (children) { // add children for a collection if (!this._collapsed) { bulletType = BulletType.Collapsible; childElements =
    - {children.Data.map(value => )} + {children.map(value => )}
; } else bulletType = BulletType.Collapsed; @@ -117,22 +116,22 @@ class TreeView extends React.Component { } @observer -export class CollectionTreeView extends CollectionSubView { +export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document) => { - var children = this.props.Document.GetT>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + const children = this.children; + if (children) { + children.splice(children.indexOf(document), 1); } } render() { - let children = this.props.Document.GetT>(KeyStore.Data, ListField); - let copyOnDrag = this.props.Document.GetBoolean(KeyStore.CopyDraggedItems, false); - let childrenElement = !children || children === FieldWaiting ? (null) : - (children.Data.map(value => - ) + const children = this.children; + let copyOnDrag = BoolCast(this.props.Document.copyDraggedItems, false); + let childrenElement = !children ? (null) : + (children.map(value => + ) ); return ( @@ -145,9 +144,9 @@ export class CollectionTreeView extends CollectionSubView { contents={this.props.Document.Title} display={"inline"} height={72} - GetValue={() => this.props.Document.Title} + GetValue={() => StrCast(this.props.Document.title)} SetValue={(value: string) => { - this.props.Document.SetText(KeyStore.Title, value); + this.props.Document.title = value; return true; }} />
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index bde50fed8..f82c6e9cb 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -3,31 +3,29 @@ import React = require("react"); import { SelectionManager } from "../../util/SelectionManager"; import { observer } from "mobx-react"; import './LinkEditor.scss'; -import { KeyStore } from '../../../fields/KeyStore'; import { props } from "bluebird"; import { DocumentView } from "./DocumentView"; -import { Document } from "../../../fields/Document"; -import { TextField } from "../../../fields/TextField"; import { link } from "fs"; +import { StrCast } from "../../../new_fields/Types"; +import { Doc } from "../../../new_fields/Doc"; interface Props { - linkDoc: Document; + linkDoc: Doc; showLinks: () => void; } @observer export class LinkEditor extends React.Component { - @observable private _nameInput: string = this.props.linkDoc.GetText(KeyStore.Title, ""); - @observable private _descriptionInput: string = this.props.linkDoc.GetText(KeyStore.LinkDescription, ""); + @observable private _nameInput: string = StrCast(this.props.linkDoc.title); + @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription); onSaveButtonPressed = (e: React.PointerEvent): void => { - console.log("view down"); e.stopPropagation(); - this.props.linkDoc.SetData(KeyStore.Title, this._nameInput, TextField); - this.props.linkDoc.SetData(KeyStore.LinkDescription, this._descriptionInput, TextField); + this.props.linkDoc.title = this._nameInput; + this.props.linkDoc.linkDescription = this._descriptionInput; this.props.showLinks(); } diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index 857da1ebb..4cac09dee 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -3,190 +3,184 @@ import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { observer } from 'mobx-react'; -import { Document } from '../fields/Document'; -import { BasicField } from '../fields/BasicField'; -import { ListField } from '../fields/ListField'; -import { Key } from '../fields/Key'; -import { Opt, Field } from '../fields/Field'; -import { Server } from '../client/Server'; - -configure({ - enforceActions: "observed" -}); - -@observer -class FieldViewer extends React.Component<{ field: BasicField }> { - render() { - return {JSON.stringify(this.props.field.Data)} ({this.props.field.Id}); - } -} - -@observer -class KeyViewer extends React.Component<{ field: Key }> { - render() { - return this.props.field.Name; - } -} - -@observer -class ListViewer extends React.Component<{ field: ListField }>{ - @observable - expanded = false; - - render() { - let content; - if (this.expanded) { - content = ( -
- {this.props.field.Data.map(field => )} -
- ); - } else { - content = <>[...] ({this.props.field.Id}); - } - return ( -
- - {content} -
- ); - } -} - -@observer -class DocumentViewer extends React.Component<{ field: Document }> { - private keyMap: ObservableMap = new ObservableMap; - - private disposer?: Lambda; - - componentDidMount() { - let f = () => { - Array.from(this.props.field._proxies.keys()).forEach(id => { - if (!this.keyMap.has(id)) { - Server.GetField(id, (field) => { - if (field && field instanceof Key) { - this.keyMap.set(id, field); - } - }); - } - }); - }; - this.disposer = this.props.field._proxies.observe(f); - f(); - } - - componentWillUnmount() { - if (this.disposer) { - this.disposer(); - } - } - - render() { - let fields = Array.from(this.props.field._proxies.entries()).map(kv => { - let key = this.keyMap.get(kv[0]); - return ( -
- ({key ? key.Name : kv[0]}): - -
- ); - }); - return ( -
- Document ({this.props.field.Id}) -
- {fields} -
-
- ); - } -} - -@observer -class DebugViewer extends React.Component<{ fieldId: string }> { - @observable - private field?: Field; - - @observable - private error?: string; - - constructor(props: { fieldId: string }) { - super(props); - this.update(); - } - - update() { - Server.GetField(this.props.fieldId, action((field: Opt) => { - this.field = field; - if (!field) { - this.error = `Field with id ${this.props.fieldId} not found`; - } - })); - - } - - render() { - let content; - if (this.field) { - // content = this.field.ToJson(); - if (this.field instanceof ListField) { - content = (); - } else if (this.field instanceof Document) { - content = (); - } else if (this.field instanceof BasicField) { - content = (); - } else if (this.field instanceof Key) { - content = (); - } else { - content = (Unrecognized field type); - } - } else if (this.error) { - content = Field {this.props.fieldId} not found ; - } else { - content = Field loading: {this.props.fieldId}; - } - return content; - } -} - -@observer -class Viewer extends React.Component { - @observable - private idToAdd: string = ''; - - @observable - private ids: string[] = []; - - @action - inputOnChange = (e: React.ChangeEvent) => { - this.idToAdd = e.target.value; - } - - @action - onKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - this.ids.push(this.idToAdd); - this.idToAdd = ""; - } - } - - render() { - return ( - <> - -
- {this.ids.map(id => )} -
- - ); - } -} - -ReactDOM.render(( -
- -
), - document.getElementById('root') -); \ No newline at end of file + +// configure({ +// enforceActions: "observed" +// }); + +// @observer +// class FieldViewer extends React.Component<{ field: BasicField }> { +// render() { +// return {JSON.stringify(this.props.field.Data)} ({this.props.field.Id}); +// } +// } + +// @observer +// class KeyViewer extends React.Component<{ field: Key }> { +// render() { +// return this.props.field.Name; +// } +// } + +// @observer +// class ListViewer extends React.Component<{ field: ListField }>{ +// @observable +// expanded = false; + +// render() { +// let content; +// if (this.expanded) { +// content = ( +//
+// {this.props.field.Data.map(field => )} +//
+// ); +// } else { +// content = <>[...] ({this.props.field.Id}); +// } +// return ( +//
+// +// {content} +//
+// ); +// } +// } + +// @observer +// class DocumentViewer extends React.Component<{ field: Document }> { +// private keyMap: ObservableMap = new ObservableMap; + +// private disposer?: Lambda; + +// componentDidMount() { +// let f = () => { +// Array.from(this.props.field._proxies.keys()).forEach(id => { +// if (!this.keyMap.has(id)) { +// Server.GetField(id, (field) => { +// if (field && field instanceof Key) { +// this.keyMap.set(id, field); +// } +// }); +// } +// }); +// }; +// this.disposer = this.props.field._proxies.observe(f); +// f(); +// } + +// componentWillUnmount() { +// if (this.disposer) { +// this.disposer(); +// } +// } + +// render() { +// let fields = Array.from(this.props.field._proxies.entries()).map(kv => { +// let key = this.keyMap.get(kv[0]); +// return ( +//
+// ({key ? key.Name : kv[0]}): +// +//
+// ); +// }); +// return ( +//
+// Document ({this.props.field.Id}) +//
+// {fields} +//
+//
+// ); +// } +// } + +// @observer +// class DebugViewer extends React.Component<{ fieldId: string }> { +// @observable +// private field?: Field; + +// @observable +// private error?: string; + +// constructor(props: { fieldId: string }) { +// super(props); +// this.update(); +// } + +// update() { +// Server.GetField(this.props.fieldId, action((field: Opt) => { +// this.field = field; +// if (!field) { +// this.error = `Field with id ${this.props.fieldId} not found`; +// } +// })); + +// } + +// render() { +// let content; +// if (this.field) { +// // content = this.field.ToJson(); +// if (this.field instanceof ListField) { +// content = (); +// } else if (this.field instanceof Document) { +// content = (); +// } else if (this.field instanceof BasicField) { +// content = (); +// } else if (this.field instanceof Key) { +// content = (); +// } else { +// content = (Unrecognized field type); +// } +// } else if (this.error) { +// content = Field {this.props.fieldId} not found ; +// } else { +// content = Field loading: {this.props.fieldId}; +// } +// return content; +// } +// } + +// @observer +// class Viewer extends React.Component { +// @observable +// private idToAdd: string = ''; + +// @observable +// private ids: string[] = []; + +// @action +// inputOnChange = (e: React.ChangeEvent) => { +// this.idToAdd = e.target.value; +// } + +// @action +// onKeyPress = (e: React.KeyboardEvent) => { +// if (e.key === "Enter") { +// this.ids.push(this.idToAdd); +// this.idToAdd = ""; +// } +// } + +// render() { +// return ( +// <> +// +//
+// {this.ids.map(id => )} +//
+// +// ); +// } +// } + +// ReactDOM.render(( +//
+// +//
), +// document.getElementById('root') +// ); \ No newline at end of file diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 7f87be45d..ae532c9e2 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,101 +1,101 @@ -import { Field, FieldId } from "./Field"; -import { Types } from "../server/Message"; -import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting"; -import { Server } from "../client/Server"; -import { Without } from "../Utils"; +// import { Field, FieldId } from "./Field"; +// import { Types } from "../server/Message"; +// import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting"; +// import { Server } from "../client/Server"; +// import { Without } from "../Utils"; -export interface SerializableOptions extends Without { - capturedIds: { [id: string]: string }; -} +// export interface SerializableOptions extends Without { +// capturedIds: { [id: string]: string }; +// } -export interface ScriptData { - script: string; - options: SerializableOptions; -} +// export interface ScriptData { +// script: string; +// options: SerializableOptions; +// } -export class ScriptField extends Field { - private _script?: CompiledScript; - get script(): CompiledScript { - return this._script!; - } - private options?: ScriptData; +// export class ScriptField extends Field { +// private _script?: CompiledScript; +// get script(): CompiledScript { +// return this._script!; +// } +// private options?: ScriptData; - constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) { - super(id); +// constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) { +// super(id); - this._script = script; +// this._script = script; - if (save) { - Server.UpdateField(this); - } - } +// if (save) { +// Server.UpdateField(this); +// } +// } - ToScriptString() { - return "new ScriptField(...)"; - } +// ToScriptString() { +// return "new ScriptField(...)"; +// } - GetValue() { - return this.script; - } +// GetValue() { +// return this.script; +// } - TrySetValue(): boolean { - throw new Error("Script fields currently can't be modified"); - } +// TrySetValue(): boolean { +// throw new Error("Script fields currently can't be modified"); +// } - UpdateFromServer() { - throw new Error("Script fields currently can't be updated"); - } +// UpdateFromServer() { +// throw new Error("Script fields currently can't be updated"); +// } - static FromJson(id: string, data: ScriptData): ScriptField { - let field = new ScriptField(undefined, id, false); - field.options = data; - return field; - } +// static FromJson(id: string, data: ScriptData): ScriptField { +// let field = new ScriptField(undefined, id, false); +// field.options = data; +// return field; +// } - init(callback: (res: Field) => any) { - const options = this.options!; - const keys = Object.keys(options.options.capturedIds); - Server.GetFields(keys).then(fields => { - let captured: { [name: string]: Field } = {}; - keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]); - const opts: ScriptOptions = { - addReturn: options.options.addReturn, - params: options.options.params, - requiredType: options.options.requiredType, - capturedVariables: captured - }; - const script = CompileScript(options.script, opts); - if (!script.compiled) { - throw new Error("Can't compile script"); - } - this._script = script; - callback(this); - }); - } +// init(callback: (res: Field) => any) { +// const options = this.options!; +// const keys = Object.keys(options.options.capturedIds); +// Server.GetFields(keys).then(fields => { +// let captured: { [name: string]: Field } = {}; +// keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]); +// const opts: ScriptOptions = { +// addReturn: options.options.addReturn, +// params: options.options.params, +// requiredType: options.options.requiredType, +// capturedVariables: captured +// }; +// const script = CompileScript(options.script, opts); +// if (!script.compiled) { +// throw new Error("Can't compile script"); +// } +// this._script = script; +// callback(this); +// }); +// } - ToJson() { - const { options, originalScript } = this.script; - let capturedIds: { [id: string]: string } = {}; - for (const capt in options.capturedVariables) { - capturedIds[options.capturedVariables[capt].Id] = capt; - } - const opts: SerializableOptions = { - ...options, - capturedIds - }; - delete (opts as any).capturedVariables; - return { - id: this.Id, - type: Types.Script, - data: { - script: originalScript, - options: opts, - }, - }; - } +// ToJson() { +// const { options, originalScript } = this.script; +// let capturedIds: { [id: string]: string } = {}; +// for (const capt in options.capturedVariables) { +// capturedIds[options.capturedVariables[capt].Id] = capt; +// } +// const opts: SerializableOptions = { +// ...options, +// capturedIds +// }; +// delete (opts as any).capturedVariables; +// return { +// id: this.Id, +// type: Types.Script, +// data: { +// script: originalScript, +// options: opts, +// }, +// }; +// } - Copy(): Field { - //Script fields are currently immutable, so we can fake copy them - return this; - } -} \ No newline at end of file +// Copy(): Field { +// //Script fields are currently immutable, so we can fake copy them +// return this; +// } +// } \ No newline at end of file diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index ec89a1194..1f9e160ce 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -1,15 +1,14 @@ import * as ReactDOM from 'react-dom'; import * as rp from 'request-promise'; -import { Documents } from '../client/documents/Documents'; -import { Server } from '../client/Server'; -import { Document } from '../fields/Document'; -import { KeyStore } from '../fields/KeyStore'; -import { ListField } from '../fields/ListField'; +import { Docs } from '../client/documents/Documents'; import { RouteStore } from '../server/RouteStore'; -import { ServerUtils } from '../server/ServerUtil'; import "./ImageUpload.scss"; import React = require('react'); -import { Opt } from '../fields/Field'; +import { DocServer } from '../client/DocServer'; +import { Opt, Doc } from '../new_fields/Doc'; +import { Cast } from '../new_fields/Types'; +import { listSpec } from '../new_fields/Schema'; +import { List } from '../new_fields/List'; @@ -38,21 +37,24 @@ const onFileLoad = async (file: any) => { const json = await res.json(); json.map(async (file: any) => { let path = window.location.origin + file; - var doc: Document = Documents.ImageDocument(path, { nativeWidth: 200, width: 200 }); + var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200 }); - const res = await rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)); + const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)); if (!res) { throw new Error("No user id returned"); } - const field = await Server.GetField(res); - let pending: Opt; - if (field instanceof Document) { - pending = await field.GetTAsync(KeyStore.OptionalRightCollection, Document); + const field = await DocServer.GetRefField(res); + let pending: Opt; + if (field instanceof Doc) { + pending = await Cast(field.optionalRightCollection, Doc); } if (pending) { - pending.GetOrCreateAsync(KeyStore.Data, ListField, list => { - list.Data.push(doc); - }); + const data = await Cast(pending.data, listSpec(Doc)); + if (data) { + data.push(doc); + } else { + pending.data = new List([doc]); + } } }); diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index a5f495477..6dfa37c1b 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -166,6 +166,4 @@ export namespace Doc { return delegate; } export const Prototype = Symbol("Prototype"); -} - -export const GetAsync = Doc.GetAsync; \ No newline at end of file +} \ No newline at end of file diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx index b08c1aebe..29327e5ad 100644 --- a/src/server/authentication/controllers/WorkspacesMenu.tsx +++ b/src/server/authentication/controllers/WorkspacesMenu.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx'; import { observer } from "mobx-react"; import './WorkspacesMenu.css'; -import { Document } from '../../../fields/Document'; import { EditableView } from '../../../client/views/EditableView'; -import { KeyStore } from '../../../fields/KeyStore'; +import { Doc, Id } from '../../../new_fields/Doc'; +import { StrCast } from '../../../new_fields/Types'; export interface WorkspaceMenuProps { - active: Document | undefined; - open: (workspace: Document) => void; + active: Doc | undefined; + open: (workspace: Doc) => void; new: () => void; - allWorkspaces: Document[]; + allWorkspaces: Doc[]; isShown: () => boolean; toggle: () => void; } @@ -60,7 +60,7 @@ export class WorkspacesMenu extends React.Component { /> {this.props.allWorkspaces.map((s, i) =>
{ e.preventDefault(); this.props.open(s); @@ -73,9 +73,9 @@ export class WorkspacesMenu extends React.Component { {i + 1} - s.Title} + GetValue={() => StrCast(s.title)} SetValue={(title: string): boolean => { - s.SetText(KeyStore.Title, title); + s.title = title; return true; }} contents={s.Title} -- cgit v1.2.3-70-g09d2 From 7a820b3b175ea52712927e75ce16e32f0ed3a131 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 27 Apr 2019 19:51:58 -0400 Subject: Compiling --- src/client/util/DragManager.ts | 8 +++---- src/client/views/MainOverlayTextBox.tsx | 17 +++++++------- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 17 ++++++-------- src/fields/RichTextField.ts | 26 ---------------------- src/new_fields/RichTextField.ts | 12 ++++++++++ src/new_fields/Types.ts | 14 ++++++------ 7 files changed, 39 insertions(+), 57 deletions(-) delete mode 100644 src/fields/RichTextField.ts create mode 100644 src/new_fields/RichTextField.ts (limited to 'src/fields') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 0174b2f54..e65f2b9ed 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -158,13 +158,13 @@ export namespace DragManager { } export class LinkDragData { - constructor(linkSourceDoc: Document, blacklist: Document[] = []) { + constructor(linkSourceDoc: Doc, blacklist: Doc[] = []) { this.linkSourceDocument = linkSourceDoc; this.blacklist = blacklist; } - droppedDocuments: Document[] = []; - linkSourceDocument: Document; - blacklist: Document[]; + droppedDocuments: Doc[] = []; + linkSourceDocument: Doc; + blacklist: Doc[]; [id: string]: any; } diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index be8d67925..a7ced36bf 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -2,16 +2,15 @@ import { action, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import { Document } from '../../fields/Document'; -import { Key } from '../../fields/Key'; -import { KeyStore } from '../../fields/KeyStore'; -import { emptyDocFunction, emptyFunction, returnTrue, returnZero } from '../../Utils'; +import { emptyFunction, returnTrue, returnZero } from '../../Utils'; import '../northstar/model/ModelExtensions'; import '../northstar/utils/Extensions'; import { DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; import "./MainOverlayTextBox.scss"; import { FormattedTextBox } from './nodes/FormattedTextBox'; +import { Doc } from '../../new_fields/Doc'; +import { NumCast } from '../../new_fields/Types'; interface MainOverlayTextBoxProps { } @@ -19,11 +18,11 @@ interface MainOverlayTextBoxProps { @observer export class MainOverlayTextBox extends React.Component { public static Instance: MainOverlayTextBox; - @observable public TextDoc?: Document = undefined; + @observable public TextDoc?: Doc = undefined; public TextScroll: number = 0; @observable _textRect: any; @observable _textXf: () => Transform = () => Transform.Identity(); - private _textFieldKey: Key = KeyStore.Data; + private _textFieldKey: string = "data"; private _textColor: string | null = null; private _textTargetDiv: HTMLDivElement | undefined; private _textProxyDiv: React.RefObject; @@ -35,7 +34,7 @@ export class MainOverlayTextBox extends React.Component } @action - SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: () => Transform) { + SetTextDoc(textDoc?: Doc, textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) { if (this._textTargetDiv) { this._textTargetDiv.style.color = this._textColor; } @@ -94,10 +93,10 @@ export class MainOverlayTextBox extends React.Component let s = 1 / this._textXf().Scale; return
+ style={{ width: `${NumCast(this.TextDoc.width)}px`, height: `${NumCast(this.TextDoc.height)}px` }}> + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 80900c450..047fbad18 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -97,7 +97,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (!NumCast(d.height)) { let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); - d.height = nw && nh ? nh / nw * d.Width() : 300; + d.height = nw && nh ? nh / nw * NumCast(d.Width) : 300; } this.bringToFront(d); }); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index c65b1ac89..7a85c9dd3 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -20,7 +20,8 @@ import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Opt, Doc } from "../../../new_fields/Doc"; import { observer } from "mobx-react"; import { InkingControl } from "../InkingControl"; -import { StrCast } from "../../../new_fields/Types"; +import { StrCast, Cast } from "../../../new_fields/Types"; +import { RichTextField } from "../../../new_fields/RichTextField"; const { buildMenuItems } = require("prosemirror-example-setup"); const { menuBar } = require("prosemirror-menu"); @@ -77,11 +78,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this._applyingChange = true; - this.props.Document.SetDataOnPrototype( - this.props.fieldKey, - JSON.stringify(state.toJSON()), - RichTextField - ); + Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n")); this._applyingChange = false; // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField); @@ -127,8 +124,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._reactionDisposer = reaction( () => { - const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined; - return field && field !== FieldWaiting ? field.Data : undefined; + const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined; + return field ? field.Data : undefined; }, field => field && this._editorView && !this._applyingChange && this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) @@ -136,8 +133,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.setupEditor(config, this.props.Document); } - private setupEditor(config: any, doc?: Document) { - let field = doc ? doc.GetT(this.props.fieldKey, RichTextField) : undefined; + private setupEditor(config: any, doc?: Doc) { + let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined; if (this._ref.current) { this._editorView = new EditorView(this._ref.current, { state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts deleted file mode 100644 index f53f48ca6..000000000 --- a/src/fields/RichTextField.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Types } from "../server/Message"; -import { FieldId } from "./Field"; - -export class RichTextField extends BasicField { - constructor(data: string = "", id?: FieldId, save: boolean = true) { - super(data, save, id); - } - - ToScriptString(): string { - return `new RichTextField(${this.Data})`; - } - - Copy() { - return new RichTextField(this.Data); - } - - ToJson() { - return { - type: Types.RichText, - data: this.Data, - id: this.Id - }; - } - -} \ No newline at end of file diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts new file mode 100644 index 000000000..156e4efd9 --- /dev/null +++ b/src/new_fields/RichTextField.ts @@ -0,0 +1,12 @@ +import { ObjectField } from "./Doc"; +import { serializable } from "serializr"; + +export class RichTextField extends ObjectField { + @serializable(true) + readonly Data: string; + + constructor(data: string) { + super(); + this.Data = data; + } +} \ No newline at end of file diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 4808d6e46..7fa18673f 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -38,10 +38,10 @@ export interface Interface { } export function Cast | ListSpec>(field: FieldResult, ctor: T): FieldResult>; -export function Cast | ListSpec>(field: FieldResult, ctor: T, defaultVal: WithoutList>): WithoutList>; -export function Cast | ListSpec>(field: FieldResult, ctor: T, defaultVal?: ToType): FieldResult> | undefined { +export function Cast | ListSpec>(field: FieldResult, ctor: T, defaultVal: WithoutList> | null): WithoutList>; +export function Cast | ListSpec>(field: FieldResult, ctor: T, defaultVal?: ToType | null): FieldResult> | undefined { if (field instanceof Promise) { - return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal; + return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal; } if (field !== undefined && !(field instanceof Promise)) { if (typeof ctor === "string") { @@ -56,18 +56,18 @@ export function Cast | ListSpec>(field: Fi return field as ToType; } } - return defaultVal; + return defaultVal === null ? undefined : defaultVal; } -export function NumCast(field: FieldResult, defaultVal: Opt = 0) { +export function NumCast(field: FieldResult, defaultVal: number | null = 0) { return Cast(field, "number", defaultVal); } -export function StrCast(field: FieldResult, defaultVal: Opt = "") { +export function StrCast(field: FieldResult, defaultVal: string | null = "") { return Cast(field, "string", defaultVal); } -export function BoolCast(field: FieldResult, defaultVal: Opt = undefined) { +export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) { return Cast(field, "boolean", defaultVal); } -- cgit v1.2.3-70-g09d2