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 7ceac5f7f4cc8172bde90c2d495da3779901ef84 Mon Sep 17 00:00:00 2001 From: Fawn Date: Fri, 19 Apr 2019 18:31:38 -0400 Subject: templating saves --- src/client/documents/Documents.ts | 8 ++++- src/client/views/DocumentDecorations.tsx | 20 +++--------- src/client/views/TemplateMenu.tsx | 13 -------- src/client/views/Templates.tsx | 55 +++++++++++++++----------------- src/client/views/nodes/DocumentView.tsx | 37 ++++++++------------- src/fields/KeyStore.ts | 4 ++- src/fields/TemplateField.ts | 35 ++++++++++++++++++++ src/server/Message.ts | 2 +- src/server/ServerUtil.ts | 3 ++ 9 files changed, 92 insertions(+), 85 deletions(-) create mode 100644 src/fields/TemplateField.ts (limited to 'src/fields') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 8db1a1c6d..eba53273f 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -32,6 +32,8 @@ import { action } from "mobx"; import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; import { AggregateFunction } from "../northstar/model/idea/idea"; +import { Template } from "../views/Templates"; +import { TemplateField } from "../../fields/TemplateField"; export interface DocumentOptions { x?: number; @@ -46,7 +48,9 @@ export interface DocumentOptions { pany?: number; page?: number; scale?: number; + baseLayout?: string; layout?: string; + template?: Template; layoutKeys?: Key[]; viewType?: number; backgroundColor?: string; @@ -95,7 +99,9 @@ export namespace Documents { if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); } if (options.backgroundColor !== undefined) { doc.SetText(KeyStore.BackgroundColor, options.backgroundColor); } if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); } + if (options.baseLayout !== undefined) { doc.SetText(KeyStore.BaseLayout, options.baseLayout); } if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } + if (options.template !== undefined) { doc.Set(KeyStore.Template, new TemplateField(options.template)); } if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } if (options.copyDraggedItems !== undefined) { doc.SetBoolean(KeyStore.CopyDraggedItems, options.copyDraggedItems); } return doc; @@ -112,7 +118,7 @@ export namespace Documents { } function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { - return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); + return assignOptions(new Document(protoId), { ...options, title: title, layout: layout , baseLayout: layout}); } function SetInstanceOptions(doc: Document, options: DocumentOptions, value: [T, { new(): U }] | Document, id?: string) { var deleg = doc.MakeDelegate(id); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3dcdc596d..2a40d7347 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -17,25 +17,11 @@ import { LinkMenu } from "./nodes/LinkMenu"; import { TemplateMenu } from "./TemplateMenu"; import React = require("react"); import { Template, Templates } from "./Templates"; +import { TemplateField } from "../../fields/TemplateField"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -// @observer -// class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent, template: Template) => void }> { -// render() { -// if (this.props.template) { -// return ( -//
  • -// this.props.toggle(event, this.props.template)} /> -// {this.props.template.Name} -//
  • -// ) -// } -// return (null); -// } -// } - @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations; @@ -409,8 +395,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let templates: Map = new Map(); let doc = SelectionManager.SelectedDocuments()[0]; Array.from(Object.values(Templates)).map(template => { - templates.set(template, doc.Template === template); + templates.set(template, doc.template.Name === template.Name); }); + // let docSrc = doc.props.Document.GetT(KeyStore.Prototype, TemplateField); + // console.log(docSrc); return (
    { @observable private _hidden: boolean = true; - @observable private _useBase: boolean = true; @observable private _templates: Map = this.props.templates; @action toggleTemplate = (event: React.ChangeEvent, template: Template): void => { - this._useBase = false; - this.props.doc.toggleBase(false); this.props.doc.changeTemplate(template); this._templates.forEach((checked, temp) => { this._templates.set(temp, false); @@ -57,15 +54,6 @@ export class TemplateMenu extends React.Component { this._templates = nextProps.templates; } - @action - toggleBase = (event: React.MouseEvent): void => { - this._useBase = true; - this.props.doc.toggleBase(true); - this._templates.forEach((checked, temp) => { - this._templates.set(temp, false); - }); - } - @action toggleTemplateActivity = (): void => { this._hidden = !this._hidden; @@ -81,7 +69,6 @@ export class TemplateMenu extends React.Component {
    this.toggleTemplateActivity()}>T
      -
    • this.toggleBase(event)} checked={this._useBase} />Base layout
    • {templateMenu}
    diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index c22b22286..3afdc711c 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -24,39 +24,34 @@ export class Template { } export namespace Templates { + export const BasicLayout = new Template("Basic layout", "{layout}"); + export const OuterCaption = new Template("Outer caption", - ` -
    -
    - {layout} -
    -
    - -
    -
    - `); + `
    {layout}
    ` + ); + export const InnerCaption = new Template("Inner caption", - ` -
    -
    - {layout} -
    -
    - -
    -
    - `); + `
    +
    + {layout} +
    +
    + +
    +
    ` + ); export const Title = new Template("Title", - ` -
    -
    - {layout} -
    -
    - {Title} -
    -
    - `); + `
    +
    + {layout} +
    +
    + {Title} +
    +
    ` + ); + + } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 12332348b..440269e36 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -21,9 +21,10 @@ import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { DocumentContentsView } from "./DocumentContentsView"; -import { Template } from "./../Templates"; +import { Template, Templates } from "./../Templates"; import "./DocumentView.scss"; import React = require("react"); +import { TemplateField } from "../../../fields/TemplateField"; export interface DocumentViewProps { @@ -92,13 +93,19 @@ export class DocumentView extends React.Component { } private _downX: number = 0; private _downY: number = 0; - private _base: string = this.props.Document.GetText(KeyStore.Layout, "

    Error loading layout data

    "); - @observable private _template: Template = new Template("", ""); + @computed get base(): string { return this.props.Document.GetText(KeyStore.BaseLayout, "

    Error loading base layout data

    "); } @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } @computed get topMost(): boolean { return this.props.isTopMost; } @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "

    Error loading layout data

    "); } @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array()); } @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array()); } + @computed get template(): Template { + let field = this.props.Document.GetT(KeyStore.Template, TemplateField); + return !field || field === FieldWaiting ? Templates.BasicLayout : field.Data; + } + set template(template: Template) { + this.props.Document.SetData(KeyStore.Template, template, TemplateField); + } screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; @@ -303,33 +310,17 @@ export class DocumentView extends React.Component { } updateLayout = (): void => { - if (this._template.Name === "") { - this.props.Document.SetText(KeyStore.Layout, this._base); - } else { - let temp = this._template.Layout; - let layout = temp.replace("{layout}", this._base); - this.props.Document.SetText(KeyStore.Layout, layout); - } - } - - @action - toggleBase = (useBase: boolean) => { - if (useBase) { - this._template = new Template("", ""); - } - this.updateLayout(); + let temp = this.template.Layout; + let layout = temp.replace("{layout}", this.base); + this.props.Document.SetText(KeyStore.Layout, layout); } @action changeTemplate = (template: Template) => { - this._template = template; + this.template = template; this.updateLayout(); } - get Template() { - return this._template; - } - @action onContextMenu = (e: React.MouseEvent): void => { e.stopPropagation(); diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 16a909eb8..1602c3776 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -19,7 +19,9 @@ export namespace KeyStore { export const Data = new Key("Data"); export const Annotations = new Key("Annotations"); export const ViewType = new Key("ViewType"); + export const BaseLayout = new Key("BaseLayout"); export const Layout = new Key("Layout"); + export const Template = new Key("Template"); export const BackgroundColor = new Key("BackgroundColor"); export const BackgroundLayout = new Key("BackgroundLayout"); export const OverlayLayout = new Key("OverlayLayout"); @@ -49,7 +51,7 @@ export namespace KeyStore { 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, + Width, Height, ZIndex, Zoom, Data, Annotations, ViewType, BaseLayout, 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 diff --git a/src/fields/TemplateField.ts b/src/fields/TemplateField.ts new file mode 100644 index 000000000..bc1e67d7b --- /dev/null +++ b/src/fields/TemplateField.ts @@ -0,0 +1,35 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; +import { Template, Templates } from "../client/views/Templates"; + + +export class TemplateField extends BasicField