From 2906c86289825ba92cccc3ccc50434ea4a4326d6 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Thu, 6 Jun 2019 21:02:14 -0400 Subject: Added computed fields --- src/client/util/Scripting.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index beaf5cb03..40e2ad6bb 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -12,6 +12,7 @@ import { Doc, Field } from '../../new_fields/Doc'; import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField'; import { List } from '../../new_fields/List'; import { RichTextField } from '../../new_fields/RichTextField'; +import { ScriptField, ComputedField } from '../../fields/ScriptField'; export interface ScriptSucccess { success: true; @@ -45,7 +46,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an return { compiled: false, errors: diagnostics }; } - let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField]; + let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; let params: any[] = [Docs, ...fieldTypes]; let compiledFunction = new Function(...paramNames, `return ${script}`); -- cgit v1.2.3-70-g09d2 From 6abf829099e4f61f2f557078f645fb9f2aa2414c Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 8 Jun 2019 22:58:23 -0400 Subject: Moved ScriptField to new_fields --- src/client/util/Scripting.ts | 2 +- src/fields/ScriptField.ts | 93 ------------------------------------------- src/new_fields/ScriptField.ts | 93 +++++++++++++++++++++++++++++++++++++++++++ src/new_fields/util.ts | 2 +- 4 files changed, 95 insertions(+), 95 deletions(-) delete mode 100644 src/fields/ScriptField.ts create mode 100644 src/new_fields/ScriptField.ts (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 40e2ad6bb..688716d5f 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -12,7 +12,7 @@ import { Doc, Field } from '../../new_fields/Doc'; import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField'; import { List } from '../../new_fields/List'; import { RichTextField } from '../../new_fields/RichTextField'; -import { ScriptField, ComputedField } from '../../fields/ScriptField'; +import { ScriptField, ComputedField } from '../../new_fields/ScriptField'; export interface ScriptSucccess { success: true; diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts deleted file mode 100644 index d4fed1a1d..000000000 --- a/src/fields/ScriptField.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ObjectField } from "../new_fields/ObjectField"; -import { CompiledScript, CompileScript } from "../client/util/Scripting"; -import { Copy, ToScriptString, Parent, SelfProxy } from "../new_fields/FieldSymbols"; -import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; -import { Deserializable } from "../client/util/SerializationHelper"; -import { computed } from "mobx"; - -function optional(propSchema: PropSchema) { - return custom(value => { - if (value !== undefined) { - return propSchema.serializer(value); - } - return SKIP; - }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { - if (jsonValue !== undefined) { - return propSchema.deserializer(jsonValue, callback, context, oldValue); - } - return SKIP; - }); -} - -const optionsSchema = createSimpleSchema({ - requiredType: true, - addReturn: true, - typecheck: true, - params: optional(map(primitive())) -}); - -const scriptSchema = createSimpleSchema({ - options: object(optionsSchema), - originalScript: true -}); - -function deserializeScript(script: ScriptField) { - const comp = CompileScript(script.script.originalScript, script.script.options); - if (!comp.compiled) { - throw new Error("Couldn't compile loaded script"); - } - (script as any).script = comp; -} - -@Deserializable("script", deserializeScript) -export class ScriptField extends ObjectField { - @serializable(object(scriptSchema)) - readonly script: CompiledScript; - - constructor(script: CompiledScript) { - super(); - - this.script = script; - } - - // 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); - // }); - // } - - [Copy](): ObjectField { - return new ScriptField(this.script); - } - - [ToScriptString]() { - return "script field"; - } -} - -@Deserializable("computed", deserializeScript) -export class ComputedField extends ScriptField { - @computed - get value() { - const val = this.script.run({ this: (this[Parent] as any)[SelfProxy] }); - if (val.success) { - return val.result; - } - return undefined; - } -} \ No newline at end of file diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts new file mode 100644 index 000000000..0839a823d --- /dev/null +++ b/src/new_fields/ScriptField.ts @@ -0,0 +1,93 @@ +import { ObjectField } from "./ObjectField"; +import { CompiledScript, CompileScript } from "../client/util/Scripting"; +import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols"; +import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; +import { Deserializable } from "../client/util/SerializationHelper"; +import { computed } from "mobx"; + +function optional(propSchema: PropSchema) { + return custom(value => { + if (value !== undefined) { + return propSchema.serializer(value); + } + return SKIP; + }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { + if (jsonValue !== undefined) { + return propSchema.deserializer(jsonValue, callback, context, oldValue); + } + return SKIP; + }); +} + +const optionsSchema = createSimpleSchema({ + requiredType: true, + addReturn: true, + typecheck: true, + params: optional(map(primitive())) +}); + +const scriptSchema = createSimpleSchema({ + options: object(optionsSchema), + originalScript: true +}); + +function deserializeScript(script: ScriptField) { + const comp = CompileScript(script.script.originalScript, script.script.options); + if (!comp.compiled) { + throw new Error("Couldn't compile loaded script"); + } + (script as any).script = comp; +} + +@Deserializable("script", deserializeScript) +export class ScriptField extends ObjectField { + @serializable(object(scriptSchema)) + readonly script: CompiledScript; + + constructor(script: CompiledScript) { + super(); + + this.script = script; + } + + // 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); + // }); + // } + + [Copy](): ObjectField { + return new ScriptField(this.script); + } + + [ToScriptString]() { + return "script field"; + } +} + +@Deserializable("computed", deserializeScript) +export class ComputedField extends ScriptField { + @computed + get value() { + const val = this.script.run({ this: this[Parent] }); + if (val.success) { + return val.result; + } + return undefined; + } +} \ No newline at end of file diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index d9c2a9866..a37abb732 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -7,7 +7,7 @@ import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy } from "./FieldSymbols"; -import { ComputedField } from "../fields/ScriptField"; +import { ComputedField } from "./ScriptField"; export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { if (SerializationHelper.IsSerializing()) { -- cgit v1.2.3-70-g09d2 From c9f77d5aab98e6e7865cdcad957d5c937631775d Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 24 Jun 2019 13:41:39 -0400 Subject: Added ReadOnly mode for docs and changed computed values a bit --- src/client/util/Scripting.ts | 12 +++++++++++- src/client/views/nodes/KeyValueBox.tsx | 6 +++--- src/fields/ScriptField.ts | 9 +++++---- src/new_fields/Doc.ts | 11 ++++++++++- src/new_fields/Proxy.ts | 4 +--- src/new_fields/util.ts | 36 +++++++++++++++++++++++++++------- 6 files changed, 59 insertions(+), 19 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 40e2ad6bb..a59d9f201 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -39,7 +39,6 @@ export interface CompileError { } export type CompileResult = CompiledScript | CompileError; - function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors) || !script) { @@ -64,10 +63,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an } } let thisParam = args.this || capturedVariables.this; + let batch: { end(): void } | undefined = undefined; try { + if (!options.editable) { + batch = Doc.MakeReadOnly(); + } const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); + if (batch) { + batch.end(); + } return { success: true, result }; } catch (error) { + if (batch) { + batch.end(); + } return { success: false, error }; } }; @@ -133,6 +142,7 @@ export interface ScriptOptions { params?: { [name: string]: string }; capturedVariables?: { [name: string]: Field }; typecheck?: boolean; + editable?: boolean; } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 3d626eef0..a4c14ae38 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -38,10 +38,11 @@ export class KeyValueBox extends React.Component { } public static SetField(doc: Doc, key: string, value: string) { let eq = value.startsWith("="); + let target = eq ? doc : Doc.GetProto(doc); value = eq ? value.substr(1) : value; let dubEq = value.startsWith(":="); value = dubEq ? value.substr(2) : value; - let options: ScriptOptions = { addReturn: true }; + let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } }; if (dubEq) options.typecheck = false; let script = CompileScript(value, options); if (!script.compiled) { @@ -49,12 +50,11 @@ export class KeyValueBox extends React.Component { } let field = new ComputedField(script); if (!dubEq) { - let res = script.run(); + let res = script.run({ this: target }); if (!res.success) return false; field = res.result; } if (Field.IsField(field, true)) { - let target = eq ? doc : Doc.GetProto(doc); target[key] = field; return true; } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index ac46ccf90..dbca74720 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -3,7 +3,7 @@ import { CompiledScript, CompileScript } from "../client/util/Scripting"; import { Copy, ToScriptString, Parent, SelfProxy } from "../new_fields/FieldSymbols"; import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; import { Deserializable } from "../client/util/SerializationHelper"; -import { computed } from "mobx"; +import { Doc } from "../new_fields/Doc"; function optional(propSchema: PropSchema) { return custom(value => { @@ -23,6 +23,7 @@ const optionsSchema = createSimpleSchema({ requiredType: true, addReturn: true, typecheck: true, + readonly: true, params: optional(map(primitive())) }); @@ -86,9 +87,9 @@ export class ScriptField extends ObjectField { @Deserializable("computed", deserializeScript) export class ComputedField extends ScriptField { - @computed - get value() { - const val = this._script.run({ this: (this[Parent] as any)[SelfProxy] }); + //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc + value(doc: Doc) { + const val = this._script.run({ this: doc }); if (val.success) { return val.result; } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 9bacf49ba..cce4fff5d 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -2,7 +2,7 @@ import { observable, action } from "mobx"; import { serializable, primitive, map, alias, list } from "serializr"; import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; import { DocServer } from "../client/DocServer"; -import { setter, getter, getField, updateFunction, deleteProperty } from "./util"; +import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util"; import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types"; import { listSpec } from "./Schema"; import { ObjectField } from "./ObjectField"; @@ -156,6 +156,15 @@ export namespace Doc { // return Cast(field, ctor); // }); // } + export function MakeReadOnly(): { end(): void } { + makeReadOnly(); + return { + end() { + makeEditable(); + } + }; + } + export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult { const self = doc[Self]; return getField(self, key, ignoreProto); diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index 130ec066e..38d874a68 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -48,9 +48,8 @@ export class ProxyField extends ObjectField { private failed = false; private promise?: Promise; - value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting { + value(): T | undefined | FieldWaiting { if (this.cache) { - callback && callback(this.cache); return this.cache; } if (this.failed) { @@ -64,7 +63,6 @@ export class ProxyField extends ObjectField { return field; })); } - callback && this.promise.then(callback); return this.promise; } } diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 8cb1db953..cc937c567 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -6,10 +6,13 @@ import { FieldValue } from "./Types"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy } from "./FieldSymbols"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; import { ComputedField } from "../fields/ScriptField"; -export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { +function _readOnlySetter(): never { + throw new Error("Documents can't be modified in read-only mode"); +} +const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { if (SerializationHelper.IsSerializing()) { target[prop] = value; return true; @@ -53,6 +56,20 @@ export const setter = action(function (target: any, prop: string | symbol | numb return true; }); +let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl; + +export function makeReadOnly() { + _setter = _readOnlySetter; +} + +export function makeEditable() { + _setter = _setterImpl; +} + +export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { + return _setter(target, prop, value, receiver); +} + export function getter(target: any, prop: string | symbol | number, receiver: any): any { if (typeof prop === "symbol") { return target.__fields[prop] || target[prop]; @@ -60,25 +77,30 @@ export function getter(target: any, prop: string | symbol | number, receiver: an if (SerializationHelper.IsSerializing()) { return target[prop]; } - return getField(target, prop); + return getFieldImpl(target, prop, receiver); } -export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { +function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { + receiver = receiver || target[SelfProxy]; const field = target.__fields[prop]; if (field instanceof ProxyField) { return field.value(); } if (field instanceof ComputedField) { - return field.value; + return field.value(receiver); } if (field === undefined && !ignoreProto && prop !== "proto") { - const proto = getField(target, "proto", true); + const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters if (proto instanceof Doc) { - return proto[prop]; + return getFieldImpl(proto[Self], prop, receiver, ignoreProto); } return undefined; } return field; + +} +export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { + return getFieldImpl(target, prop, undefined, ignoreProto); } export function deleteProperty(target: any, prop: string | number | symbol) { -- cgit v1.2.3-70-g09d2 From 25fdbe3b9159473a9249246f6c208b6e1220dbbb Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 3 Jul 2019 14:46:44 -0400 Subject: Changed how scripting works --- src/client/documents/Documents.ts | 25 +++------------------- src/client/util/Scripting.ts | 45 +++++++++++++++++++++++++++++++-------- src/new_fields/Doc.ts | 2 ++ src/new_fields/List.ts | 5 ++++- src/new_fields/RichTextField.ts | 2 ++ src/new_fields/ScriptField.ts | 4 +++- src/new_fields/URLField.ts | 11 +++++----- 7 files changed, 56 insertions(+), 38 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7d7a1f02a..7a976e7d7 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -36,6 +36,7 @@ import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; import { LinkManager } from "../util/LinkManager"; import { DocumentManager } from "../util/DocumentManager"; +import { Scripting } from "../util/Scripting"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); @@ -385,26 +386,6 @@ export namespace Docs { `); } - /* - - this template requires an additional style setting on the collectionView-cont to make the layout relative - -.collectionView-cont { - position: relative; - width: 100%; - height: 100%; } - */ - function Percentaption() { - return (` -
-
- {layout} -
-
- -
-
- `); - } -} \ No newline at end of file + +Scripting.addGlobal("Docs", Docs); \ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 30a05154a..3156c4f43 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,12 +7,7 @@ let ts = (window as any).ts; // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; -import { Docs } from "../documents/Documents"; import { Doc, Field } from '../../new_fields/Doc'; -import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField'; -import { List } from '../../new_fields/List'; -import { RichTextField } from '../../new_fields/RichTextField'; -import { ScriptField, ComputedField } from '../../new_fields/ScriptField'; export interface ScriptSucccess { success: true; @@ -38,6 +33,34 @@ export interface CompileError { errors: any[]; } +export namespace Scripting { + export function addGlobal(global: { name: string }): void; + export function addGlobal(name: string, global: any): void; + export function addGlobal(nameOrGlobal: any, global?: any) { + let n: string; + let obj: any; + if (global !== undefined && typeof nameOrGlobal === "string") { + n = nameOrGlobal; + obj = global; + } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") { + n = nameOrGlobal.name; + obj = nameOrGlobal; + } else { + throw new Error("Must either register an object with a name, or give a name and an object"); + } + if (scriptingGlobals.hasOwnProperty(n)) { + throw new Error(`Global with name ${n} is already registered, choose another name`); + } + scriptingGlobals[n] = obj; + } +} + +export function scriptingGlobal(constructor: { new(...args: any[]): any }) { + Scripting.addGlobal(constructor); +} + +const scriptingGlobals: { [name: string]: any } = {}; + export type CompileResult = CompiledScript | CompileError; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); @@ -45,9 +68,11 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an return { compiled: false, errors: diagnostics }; } - let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; - let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; - let params: any[] = [Docs, ...fieldTypes]; + let paramNames = Object.keys(scriptingGlobals); + let params = paramNames.map(key => scriptingGlobals[key]); + // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; + // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; + // let params: any[] = [Docs, ...fieldTypes]; let compiledFunction = new Function(...paramNames, `return ${script}`); let { capturedVariables = {} } = options; let run = (args: { [name: string]: any } = {}): ScriptResult => { @@ -178,4 +203,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); return Run(outputText, paramNames, diagnostics, script, options); -} \ No newline at end of file +} + +Scripting.addGlobal(CompileScript); \ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 29d35e19f..c361e3032 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -8,6 +8,7 @@ import { listSpec } from "./Schema"; import { ObjectField } from "./ObjectField"; import { RefField, FieldId } from "./RefField"; import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols"; +import { scriptingGlobal } from "../client/util/Scripting"; export namespace Field { export function toScriptString(field: Field): string { @@ -55,6 +56,7 @@ export function DocListCast(field: FieldResult): Doc[] { export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); +@scriptingGlobal @Deserializable("doc").withFields(["id"]) export class Doc extends RefField { constructor(id?: FieldId, forceSave?: boolean) { diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index f1e4c4721..a2133a990 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -7,6 +7,7 @@ import { ObjectField } from "./ObjectField"; import { RefField } from "./RefField"; import { ProxyField } from "./Proxy"; import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols"; +import { Scripting } from "../client/util/Scripting"; const listHandlers: any = { /// Mutator methods @@ -294,4 +295,6 @@ class ListImpl extends ObjectField { } } export type List = ListImpl & (T | (T extends RefField ? Promise : never))[]; -export const List: { new (fields?: T[]): List } = ListImpl as any; \ No newline at end of file +export const List: { new (fields?: T[]): List } = ListImpl as any; + +Scripting.addGlobal("List", List); \ No newline at end of file diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 89d077a47..78a3a4067 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -2,7 +2,9 @@ import { ObjectField } from "./ObjectField"; import { serializable } from "serializr"; import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString } from "./FieldSymbols"; +import { scriptingGlobal } from "../client/util/Scripting"; +@scriptingGlobal @Deserializable("RichTextField") export class RichTextField extends ObjectField { @serializable(true) diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts index 3d56e9374..e2994ed70 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -1,5 +1,5 @@ import { ObjectField } from "./ObjectField"; -import { CompiledScript, CompileScript } from "../client/util/Scripting"; +import { CompiledScript, CompileScript, scriptingGlobal } from "../client/util/Scripting"; import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols"; import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; import { Deserializable } from "../client/util/SerializationHelper"; @@ -40,6 +40,7 @@ function deserializeScript(script: ScriptField) { (script as any).script = comp; } +@scriptingGlobal @Deserializable("script", deserializeScript) export class ScriptField extends ObjectField { @serializable(object(scriptSchema)) @@ -81,6 +82,7 @@ export class ScriptField extends ObjectField { } } +@scriptingGlobal @Deserializable("computed", deserializeScript) export class ComputedField extends ScriptField { //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts index 4a2841fb6..d935a61af 100644 --- a/src/new_fields/URLField.ts +++ b/src/new_fields/URLField.ts @@ -2,6 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom } from "serializr"; import { ObjectField } from "./ObjectField"; import { ToScriptString, Copy } from "./FieldSymbols"; +import { Scripting, scriptingGlobal } from "../client/util/Scripting"; function url() { return custom( @@ -37,8 +38,8 @@ export abstract class URLField extends ObjectField { } } -@Deserializable("audio") export class AudioField extends URLField { } -@Deserializable("image") export class ImageField extends URLField { } -@Deserializable("video") export class VideoField extends URLField { } -@Deserializable("pdf") export class PdfField extends URLField { } -@Deserializable("web") export class WebField extends URLField { } \ No newline at end of file +@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { } +@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { } +@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { } +@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { } +@scriptingGlobal @Deserializable("web") export class WebField extends URLField { } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 60ceef0a3a8c11d85434a154e542424d34d9562c Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 13 Jul 2019 16:46:49 -0400 Subject: Refactored style in DocDecs and added metadata entry to DocDecs --- src/client/util/Scripting.ts | 3 +- src/client/views/DocumentDecorations.scss | 44 ++++++++++-------- src/client/views/DocumentDecorations.tsx | 31 +++++++++---- src/client/views/MetadataEntryMenu.scss | 10 +++++ src/client/views/MetadataEntryMenu.tsx | 74 +++++++++++++++++++++++++++++++ src/client/views/nodes/KeyValueBox.tsx | 31 ++++++++++--- src/client/views/nodes/KeyValuePair.tsx | 8 +--- src/new_fields/Doc.ts | 9 ++++ 8 files changed, 169 insertions(+), 41 deletions(-) create mode 100644 src/client/views/MetadataEntryMenu.scss create mode 100644 src/client/views/MetadataEntryMenu.tsx (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 3156c4f43..62c2cfe85 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -33,6 +33,8 @@ export interface CompileError { errors: any[]; } +export type CompileResult = CompiledScript | CompileError; + export namespace Scripting { export function addGlobal(global: { name: string }): void; export function addGlobal(name: string, global: any): void; @@ -61,7 +63,6 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) { const scriptingGlobals: { [name: string]: any } = {}; -export type CompileResult = CompiledScript | CompileError; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors) || !script) { diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 2d430512b..1afc5c147 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,6 +1,7 @@ @import "globalCssVariables"; $linkGap : 3px; + .documentDecorations { position: absolute; } @@ -52,15 +53,16 @@ $linkGap : 3px; grid-column-start: 5; grid-column-end: 7; } - - #documentDecorations-borderRadius{ + + #documentDecorations-borderRadius { grid-column-start: 5; grid-column-end: 7; border-radius: 100%; - .borderRadiusTooltip{ - width:10px; - height:10px; - position:absolute; + + .borderRadiusTooltip { + width: 10px; + height: 10px; + position: absolute; } } @@ -68,8 +70,9 @@ $linkGap : 3px; #documentDecorations-bottomRightResizer { cursor: nwse-resize; } + #documentDecorations-bottomRightResizer { - grid-row:4; + grid-row: 4; } #documentDecorations-topRightResizer, @@ -86,7 +89,8 @@ $linkGap : 3px; #documentDecorations-rightResizer { cursor: ew-resize; } - .title{ + + .title { background: $alt-accent; grid-column-start: 3; grid-column-end: 4; @@ -129,7 +133,6 @@ $linkGap : 3px; .linkFlyout { grid-column: 2/4; - margin-top: $linkGap; } .linkButton-empty:hover { @@ -145,6 +148,7 @@ $linkGap : 3px; } .link-button-container { + margin-top: $linkGap; grid-column: 1/4; width: auto; height: auto; @@ -152,9 +156,12 @@ $linkGap : 3px; flex-direction: row; } +.linkButtonWrapper { + pointer-events: auto; + padding-right: 5px; +} + .linkButton-linker { - margin-left: 5px; - margin-top: $linkGap; height: 20px; width: 20px; text-align: center; @@ -169,7 +176,8 @@ $linkGap : 3px; transform: scale(1.05); } -.linkButton-empty, .linkButton-nonempty { +.linkButton-empty, +.linkButton-nonempty { height: 20px; width: 20px; border-radius: 50%; @@ -194,9 +202,6 @@ $linkGap : 3px; } .templating-menu { - position: absolute; - bottom: 0; - left: 50px; pointer-events: auto; text-transform: uppercase; letter-spacing: 2px; @@ -208,15 +213,17 @@ $linkGap : 3px; align-items: center; } -.fa-icon-link { +.documentdecorations-icon { margin-top: 3px; } -.templating-button { + +.templating-button, +.docDecs-tagButton { width: 20px; height: 20px; border-radius: 50%; opacity: 0.9; - font-size:14; + font-size: 14; background-color: $dark-color; color: $light-color; text-align: center; @@ -238,6 +245,7 @@ $linkGap : 3px; background-color: $light-color-secondary; padding: 2px 12px; list-style: none; + .templateToggle { text-align: left; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index c24d66ee4..56fbd75a0 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faLink } from '@fortawesome/free-solid-svg-icons'; +import { faLink, faTag } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -27,11 +27,13 @@ import React = require("react"); import { RichTextField } from '../../new_fields/RichTextField'; import { LinkManager } from '../util/LinkManager'; import { ObjectField } from '../../new_fields/ObjectField'; +import { MetadataEntryMenu } from './MetadataEntryMenu'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; library.add(faLink); +library.add(faTag); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -596,8 +598,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (!canEmbed) return (null); return (
-
- +
+
); @@ -610,10 +612,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._textDoc = thisDoc; return (
-
+
{/* */} - T -
+
); @@ -634,6 +635,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } + get metadataMenu() { + return ( +
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} />}>{/* tfs: @bcz This might need to be the data document? */} +
+
+
+ ); + } + render() { var bounds = this.Bounds; let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; @@ -723,10 +735,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
- +
- +
+ +
+ {this.metadataMenu} {this.considerEmbed()} {/* {this.considerTooltip()} */} diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss new file mode 100644 index 000000000..73e5b6a73 --- /dev/null +++ b/src/client/views/MetadataEntryMenu.scss @@ -0,0 +1,10 @@ +.metadataEntry-input { + width: 40%; + margin-left: 5px; + margin-right: 5px; +} + +.metadataEntry-outerDiv { + display: flex; + width: 300px; +} \ No newline at end of file diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx new file mode 100644 index 000000000..0dc7e0220 --- /dev/null +++ b/src/client/views/MetadataEntryMenu.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import "./MetadataEntryMenu.scss"; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { KeyValueBox } from './nodes/KeyValueBox'; +import { Doc } from '../../new_fields/Doc'; + +export type DocLike = Doc | Doc[] | Promise | Promise; +export interface MetadataEntryProps { + docs: DocLike | (() => DocLike); + onError?: () => boolean; +} + +@observer +export class MetadataEntryMenu extends React.Component{ + @observable private _currentKey: string = ""; + @observable private _currentValue: string = ""; + + @action + onKeyChange = (e: React.ChangeEvent) => { + this._currentKey = e.target.value; + } + + @action + onValueChange = (e: React.ChangeEvent) => { + this._currentValue = e.target.value; + } + + onValueKeyDown = async (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + const script = KeyValueBox.CompileKVPScript(this._currentValue); + if (!script) return; + let doc = this.props.docs; + if (typeof doc === "function") { + doc = doc(); + } + doc = await doc; + let success: boolean; + if (doc instanceof Doc) { + success = KeyValueBox.ApplyKVPScript(doc, this._currentKey, script); + } else { + success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)); + } + if (!success) { + if (this.props.onError) { + if (this.props.onError()) { + this.clearInputs(); + } + } else { + this.clearInputs(); + } + } else { + this.clearInputs(); + } + } + } + + @action + clearInputs = () => { + this._currentKey = ""; + this._currentValue = ""; + } + + render() { + return ( +
+ Key: + + Value: + +
+ ); + } +} \ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index eb8abf2c7..c9dd9a64e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -2,7 +2,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { CompileScript, ScriptOptions } from "../../util/Scripting"; +import { CompileScript, ScriptOptions, CompiledScript } from "../../util/Scripting"; import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; import { KeyValuePair } from "./KeyValuePair"; @@ -21,6 +21,12 @@ import { ImageField } from "../../../new_fields/URLField"; import { SelectionManager } from "../../util/SelectionManager"; import { listSpec } from "../../../new_fields/Schema"; +export type KVPScript = { + script: CompiledScript; + type: "computed" | "script" | false; + onDelegate: boolean; +}; + @observer export class KeyValueBox extends React.Component { private _mainCont = React.createRef(); @@ -48,22 +54,27 @@ export class KeyValueBox extends React.Component { } } } - public static SetField(doc: Doc, key: string, value: string) { + public static CompileKVPScript(value: string): KVPScript | undefined { let eq = value.startsWith("="); - let target = eq ? doc : Doc.GetProto(doc); value = eq ? value.substr(1) : value; - let dubEq = value.startsWith(":=") ? 1 : value.startsWith(";=") ? 2 : 0; + const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false; value = dubEq ? value.substr(2) : value; let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } }; if (dubEq) options.typecheck = false; let script = CompileScript(value, options); if (!script.compiled) { - return false; + return undefined; } + return { script, type: dubEq, onDelegate: eq }; + } + + public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean { + const { script, type, onDelegate } = kvpScript; + const target = onDelegate ? doc : Doc.GetProto(doc); let field: Field; - if (dubEq === 1) { + if (type === "computed") { field = new ComputedField(script); - } else if (dubEq === 2) { + } else if (type === "script") { field = new ScriptField(script); } else { let res = script.run({ this: target }); @@ -77,6 +88,12 @@ export class KeyValueBox extends React.Component { return false; } + public static SetField(doc: Doc, key: string, value: string) { + const script = this.CompileKVPScript(value); + if (!script) return false; + return this.ApplyKVPScript(doc, key, script); + } + onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index b5db52ac7..209782242 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -92,13 +92,7 @@ export class KeyValuePair extends React.Component { contents={contents} height={36} GetValue={() => { - const onDelegate = Object.keys(props.Document).includes(props.fieldKey); - - let field = FieldValue(props.Document[props.fieldKey]); - if (Field.IsField(field)) { - return (onDelegate ? "=" : "") + Field.toScriptString(field); - } - return ""; + return Field.toKeyValueString(props.Document, props.fieldKey); }} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)}> diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 0e5f8b4d9..c5f9e7adf 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -12,6 +12,15 @@ import { scriptingGlobal } from "../client/util/Scripting"; import { List } from "./List"; export namespace Field { + export function toKeyValueString(doc: Doc, key: string): string { + const onDelegate = Object.keys(doc).includes(key); + + let field = FieldValue(doc[key]); + if (Field.IsField(field)) { + return (onDelegate ? "=" : "") + Field.toScriptString(field); + } + return ""; + } export function toScriptString(field: Field): string { if (typeof field === "string") { return `"${field}"`; -- cgit v1.2.3-70-g09d2