diff options
45 files changed, 970 insertions, 417 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index e4196600e..822a06024 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,6 +25,10 @@          {              "type": "chrome",              "request": "launch", +            "runtimeArgs": [ +                "--enable-logging", +                "--v=1" +            ],              "name": "Launch Chrome against Dash server",              "sourceMaps": true,              "breakOnLoad": true, diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 8c64d2b2f..cb460799f 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -5,7 +5,6 @@ import { Utils, emptyFunction } from '../Utils';  import { SerializationHelper } from './util/SerializationHelper';  import { RefField } from '../new_fields/RefField';  import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; -import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';  /**   * This class encapsulates the transfer and cross-client synchronization of @@ -26,7 +25,6 @@ export namespace DocServer {      // this client's distinct GUID created at initialization      let GUID: string;      // indicates whether or not a document is currently being udpated, and, if so, its id -    let updatingId: string | undefined;      export function init(protocol: string, hostname: string, port: number, identifier: string) {          _cache = {}; @@ -126,12 +124,11 @@ export namespace DocServer {              // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.              const deserializeField = getSerializedField.then(async fieldJson => {                  // deserialize -                const field = SerializationHelper.Deserialize(fieldJson); +                const field = await SerializationHelper.Deserialize(fieldJson);                  // either way, overwrite or delete any promises cached at this id (that we inserted as flags                  // to indicate that the field was in the process of being fetched). Now everything                  // should be an actual value within or entirely absent from the cache.                  if (field !== undefined) { -                    await field.proto;                      _cache[id] = field;                  } else {                      delete _cache[id]; @@ -202,18 +199,18 @@ export namespace DocServer {          // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.          const deserializeFields = getSerializedFields.then(async fields => {              const fieldMap: { [id: string]: RefField } = {}; -            const protosToLoad: any = []; +            // const protosToLoad: any = [];              for (const field of fields) {                  if (field !== undefined) {                      // deserialize -                    let deserialized: any = SerializationHelper.Deserialize(field); +                    let deserialized = await SerializationHelper.Deserialize(field);                      fieldMap[field.id] = deserialized;                      // adds to a list of promises that will be awaited asynchronously -                    protosToLoad.push(deserialized.proto); +                    // protosToLoad.push(deserialized.proto);                  }              }              // this actually handles the loading of prototypes -            await Promise.all(protosToLoad); +            // await Promise.all(protosToLoad);              return fieldMap;          }); @@ -304,9 +301,6 @@ export namespace DocServer {      }      function _UpdateFieldImpl(id: string, diff: any) { -        if (id === updatingId) { -            return; -        }          Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });      } @@ -329,11 +323,7 @@ export namespace DocServer {              // extract this Doc's update handler              const handler = f[HandleUpdate];              if (handler) { -                // set the 'I'm currently updating this Doc' flag -                updatingId = id;                  handler.call(f, diff.diff); -                // reset to indicate no ongoing updates -                updatingId = undefined;              }          };          // check the cache for the field diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index d4085cf76..d69378d0e 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -1,5 +1,5 @@  import * as request from "request-promise"; -import { Doc, Field } from "../../new_fields/Doc"; +import { Doc, Field, Opt } from "../../new_fields/Doc";  import { Cast } from "../../new_fields/Types";  import { ImageField } from "../../new_fields/URLField";  import { List } from "../../new_fields/List"; @@ -8,10 +8,21 @@ import { RouteStore } from "../../server/RouteStore";  import { Utils } from "../../Utils";  import { CompileScript } from "../util/Scripting";  import { ComputedField } from "../../new_fields/ScriptField"; +import { InkData } from "../../new_fields/InkField"; -export enum Services { +type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor, analyzer: AnalysisApplier }; +type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>; +type AnalysisApplier = (target: Doc, relevantKeys: string[], ...args: any) => any; +type BodyConverter<D> = (data: D) => string; +type Converter = (results: any) => Field; + +export type Tag = { name: string, confidence: number }; +export type Rectangle = { top: number, left: number, width: number, height: number }; + +export enum Service {      ComputerVision = "vision", -    Face = "face" +    Face = "face", +    Handwriting = "handwriting"  }  export enum Confidence { @@ -23,11 +34,6 @@ export enum Confidence {      Excellent = 0.95  } -export type Tag = { name: string, confidence: number }; -export type Rectangle = { top: number, left: number, width: number, height: number }; -export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle }; -export type Converter = (results: any) => Field; -  /**   * A file that handles all interactions with Microsoft Azure's Cognitive   * Services APIs. These machine learning endpoints allow basic data analytics for @@ -35,19 +41,36 @@ export type Converter = (results: any) => Field;   */  export namespace CognitiveServices { +    const executeQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => { +        return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => { +            let apiKey = await response.text(); +            if (!apiKey) { +                console.log(`No API key found for ${service}: ensure index.ts has access to a .env file in your root directory`); +                return undefined; +            } + +            let results: Opt<R>; +            try { +                results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json)); +            } catch { +                results = undefined; +            } +            return results; +        }); +    }; +      export namespace Image { -        export const analyze = async (imageUrl: string, service: Services) => { -            return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => { -                let apiKey = await response.text(); -                if (!apiKey) { -                    return undefined; -                } +        export const Manager: APIManager<string> = { + +            converter: (imageUrl: string) => JSON.stringify({ url: imageUrl }), + +            requester: async (apiKey: string, body: string, service: Service) => {                  let uriBase;                  let parameters;                  switch (service) { -                    case Services.Face: +                    case Service.Face:                          uriBase = 'face/v1.0/detect';                          parameters = {                              'returnFaceId': 'true', @@ -56,7 +79,7 @@ export namespace CognitiveServices {                                  'emotion,hair,makeup,occlusion,accessories,blur,exposure,noise'                          };                          break; -                    case Services.ComputerVision: +                    case Service.ComputerVision:                          uriBase = 'vision/v2.0/analyze';                          parameters = {                              'visualFeatures': 'Categories,Description,Color,Objects,Tags,Adult', @@ -69,42 +92,40 @@ export namespace CognitiveServices {                  const options = {                      uri: 'https://eastus.api.cognitive.microsoft.com/' + uriBase,                      qs: parameters, -                    body: `{"url": "${imageUrl}"}`, +                    body: body,                      headers: {                          'Content-Type': 'application/json',                          'Ocp-Apim-Subscription-Key': apiKey                      }                  }; -                let results: any; -                try { -                    results = await request.post(options).then(response => JSON.parse(response)); -                } catch (e) { -                    results = undefined; -                } -                return results; -            }); -        }; +                return request.post(options); +            }, -        const analyzeDocument = async (target: Doc, service: Services, converter: Converter, storageKey: string) => { -            let imageData = Cast(target.data, ImageField); -            if (!imageData || await Cast(target[storageKey], Doc)) { -                return; -            } -            let toStore: any; -            let results = await analyze(imageData.url.href, service); -            if (!results) { -                toStore = "Cognitive Services could not process the given image URL."; -            } else { -                if (!results.length) { -                    toStore = converter(results); +            analyzer: async (target: Doc, keys: string[], service: Service, converter: Converter) => { +                let imageData = Cast(target.data, ImageField); +                let storageKey = keys[0]; +                if (!imageData || await Cast(target[storageKey], Doc)) { +                    return; +                } +                let toStore: any; +                let results = await executeQuery<string, any>(service, Manager, imageData.url.href); +                if (!results) { +                    toStore = "Cognitive Services could not process the given image URL.";                  } else { -                    toStore = results.length > 0 ? converter(results) : "Empty list returned."; +                    if (!results.length) { +                        toStore = converter(results); +                    } else { +                        toStore = results.length > 0 ? converter(results) : "Empty list returned."; +                    }                  } +                target[storageKey] = toStore;              } -            target[storageKey] = toStore; +          }; +        export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle }; +          export const generateMetadata = async (target: Doc, threshold: Confidence = Confidence.Excellent) => {              let converter = (results: any) => {                  let tagDoc = new Doc; @@ -118,7 +139,7 @@ export namespace CognitiveServices {                  tagDoc.confidence = threshold;                  return tagDoc;              }; -            analyzeDocument(target, Services.ComputerVision, converter, "generatedTags"); +            Manager.analyzer(target, ["generatedTags"], Service.ComputerVision, converter);          };          export const extractFaces = async (target: Doc) => { @@ -127,9 +148,88 @@ export namespace CognitiveServices {                  results.map((face: Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!));                  return faceDocs;              }; -            analyzeDocument(target, Services.Face, converter, "faces"); +            Manager.analyzer(target, ["faces"], Service.Face, converter); +        }; + +    } + +    export namespace Inking { + +        export const Manager: APIManager<InkData> = { + +            converter: (inkData: InkData): string => { +                let entries = inkData.entries(), next = entries.next(); +                let strokes: AzureStrokeData[] = [], id = 0; +                while (!next.done) { +                    strokes.push({ +                        id: id++, +                        points: next.value[1].pathData.map(point => `${point.x},${point.y}`).join(","), +                        language: "en-US" +                    }); +                    next = entries.next(); +                } +                return JSON.stringify({ +                    version: 1, +                    language: "en-US", +                    unit: "mm", +                    strokes: strokes +                }); +            }, + +            requester: async (apiKey: string, body: string) => { +                let xhttp = new XMLHttpRequest(); +                let serverAddress = "https://api.cognitive.microsoft.com"; +                let endpoint = serverAddress + "/inkrecognizer/v1.0-preview/recognize"; + +                let promisified = (resolve: any, reject: any) => { +                    xhttp.onreadystatechange = function () { +                        if (this.readyState === 4) { +                            let result = xhttp.responseText; +                            switch (this.status) { +                                case 200: +                                    return resolve(result); +                                case 400: +                                default: +                                    return reject(result); +                            } +                        } +                    }; + +                    xhttp.open("PUT", endpoint, true); +                    xhttp.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey); +                    xhttp.setRequestHeader('Content-Type', 'application/json'); +                    xhttp.send(body); +                }; + +                return new Promise<any>(promisified); +            }, + +            analyzer: async (target: Doc, keys: string[], inkData: InkData) => { +                let results = await executeQuery<InkData, any>(Service.Handwriting, Manager, inkData); +                if (results) { +                    results.recognitionUnits && (results = results.recognitionUnits); +                    target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis"); +                    let recognizedText = results.map((item: any) => item.recognizedText); +                    let individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1); +                    target[keys[1]] = individualWords.join(" "); +                } +            } +          }; +        export interface AzureStrokeData { +            id: number; +            points: string; +            language?: string; +        } + +        export interface HandwritingUnit { +            version: number; +            language: string; +            unit: string; +            strokes: AzureStrokeData[]; +        } +      }  }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7563fda20..3859f2255 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -38,6 +38,7 @@ import { LinkManager } from "../util/LinkManager";  import { DocumentManager } from "../util/DocumentManager";  import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";  import { Scripting } from "../util/Scripting"; +import { ButtonBox } from "../views/nodes/ButtonBox";  var requestImageSize = require('../util/request-image-size');  var path = require('path'); @@ -56,7 +57,9 @@ export enum DocumentType {      IMPORT = "import",      LINK = "link",      LINKDOC = "linkdoc", -    TEMPLATE = "template" +    BUTTON = "button", +    TEMPLATE = "template", +    EXTENSION = "extension"  }  export interface DocumentOptions { @@ -161,6 +164,9 @@ export namespace Docs {                  data: new List<Doc>(),                  layout: { view: EmptyBox },                  options: {} +            }], +            [DocumentType.BUTTON, { +                layout: { view: ButtonBox },              }]          ]); @@ -276,7 +282,7 @@ export namespace Docs {           * only when creating a DockDocument from the current user's already existing           * main document.           */ -        export function InstanceFromProto(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) { +        export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string) {              const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);              if (!("author" in protoProps)) { @@ -305,9 +311,11 @@ export namespace Docs {           * @param options initial values to apply to this new delegate           * @param value the data to store in this new delegate           */ -        function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value: D) { +        function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D) {              const deleg = Doc.MakeDelegate(proto); -            deleg.data = value; +            if (value !== undefined) { +                deleg.data = value; +            }              return Doc.assign(deleg, options);          } @@ -410,6 +418,10 @@ export namespace Docs {              return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });          } +        export function ButtonDocument(options?: DocumentOptions) { +            return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) }); +        } +          export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {              return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);          } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 323908302..5271f2f5d 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -412,7 +412,6 @@ export namespace DragManager {          };          let hideDragElements = () => { -            SelectionManager.SetIsDragging(false);              dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));              eles.map(ele => (ele.hidden = false));          }; @@ -426,11 +425,13 @@ export namespace DragManager {          AbortDrag = () => {              hideDragElements(); +            SelectionManager.SetIsDragging(false);              endDrag();          };          const upHandler = (e: PointerEvent) => {              hideDragElements();              dispatchDrag(eles, e, dragData, options, finishDrag); +            SelectionManager.SetIsDragging(false);              endDrag();          };          document.addEventListener("pointermove", moveHandler, true); diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 46dc320b0..1d0916ac0 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -52,10 +52,10 @@ export namespace Scripting {          } else {              throw new Error("Must either register an object with a name, or give a name and an object");          } -        if (scriptingGlobals.hasOwnProperty(n)) { +        if (_scriptingGlobals.hasOwnProperty(n)) {              throw new Error(`Global with name ${n} is already registered, choose another name`);          } -        scriptingGlobals[n] = obj; +        _scriptingGlobals[n] = obj;      }      export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) { @@ -188,6 +188,10 @@ class ScriptingCompilerHost {  export type Traverser = (node: ts.Node, indentation: string) => boolean | void;  export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser }; +export type Transformer = { +    transformer: ts.TransformerFactory<ts.SourceFile>, +    getVars?: () => { capturedVariables: { [name: string]: Field } } +};  export interface ScriptOptions {      requiredType?: string;      addReturn?: boolean; @@ -196,7 +200,7 @@ export interface ScriptOptions {      typecheck?: boolean;      editable?: boolean;      traverser?: TraverserParam; -    transformer?: ts.TransformerFactory<ts.SourceFile>; +    transformer?: Transformer;      globals?: { [name: string]: any };  } @@ -213,6 +217,27 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp          Scripting.setScriptingGlobals(options.globals);      }      let host = new ScriptingCompilerHost; +    if (options.traverser) { +        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); +        const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser; +        const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; +        forEachNode(sourceFile, onEnter, onLeave); +    } +    if (options.transformer) { +        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); +        const result = ts.transform(sourceFile, [options.transformer.transformer]); +        if (options.transformer.getVars) { +            const newCaptures = options.transformer.getVars(); +            // tslint:disable-next-line: prefer-object-spread +            options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any; +        } +        const transformed = result.transformed; +        const printer = ts.createPrinter({ +            newLine: ts.NewLineKind.LineFeed +        }); +        script = printer.printFile(transformed[0]); +        result.dispose(); +    }      let paramNames: string[] = [];      if ("this" in params || "this" in capturedVariables) {          paramNames.push("this"); @@ -227,26 +252,11 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp      });      for (const key in capturedVariables) {          if (key === "this") continue; +        const val = capturedVariables[key];          paramNames.push(key); -        paramList.push(`${key}: ${capturedVariables[key].constructor.name}`); +        paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);      }      let paramString = paramList.join(", "); -    if (options.traverser) { -        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); -        const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser; -        const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; -        forEachNode(sourceFile, onEnter, onLeave); -    } -    if (options.transformer) { -        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); -        const result = ts.transform(sourceFile, [options.transformer]); -        const transformed = result.transformed; -        const printer = ts.createPrinter({ -            newLine: ts.NewLineKind.LineFeed -        }); -        script = printer.printFile(transformed[0]); -        result.dispose(); -    }      let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {          ${addReturn ? `return ${script};` : script}      })`; diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index dca539f3b..034be8f67 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,9 +1,14 @@  import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr"; -import { Field } from "../../new_fields/Doc"; +import { Field, Doc } from "../../new_fields/Doc";  import { ClientUtils } from "./ClientUtils"; +let serializing = 0; +export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) { +    serializing++; +    cb(err, newValue); +    serializing--; +}  export namespace SerializationHelper { -    let serializing: number = 0;      export function IsSerializing() {          return serializing > 0;      } @@ -17,18 +22,18 @@ export namespace SerializationHelper {              return obj;          } -        serializing += 1; +        serializing++;          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; +        serializing--;          return json;      } -    export function Deserialize(obj: any): any { +    export async function Deserialize(obj: any): Promise<any> {          if (obj === undefined || obj === null) {              return undefined;          } @@ -37,7 +42,6 @@ export namespace SerializationHelper {              return obj;          } -        serializing += 1;          if (!obj.__type) {              if (ClientUtils.RELEASE) {                  console.warn("No property 'type' found in JSON."); @@ -52,16 +56,15 @@ export namespace SerializationHelper {          }          const type = serializationTypes[obj.__type]; -        const value = deserialize(type.ctor, obj); +        const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result)));          if (type.afterDeserialize) { -            type.afterDeserialize(value); +            await type.afterDeserialize(value);          } -        serializing -= 1;          return value;      }  } -let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {}; +let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};  let reverseMap: { [ctor: string]: string } = {};  export interface DeserializableOpts { @@ -69,7 +72,7 @@ export interface DeserializableOpts {      withFields(fields: string[]): Function;  } -export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts; +export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts;  export function Deserializable(constructor: { new(...args: any[]): any }): void;  export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {      function addToMap(name: string, ctor: { new(...args: any[]): any }) { @@ -88,15 +91,15 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin      if (typeof constructor === "string") {          return Object.assign((ctor: { new(...args: any[]): any }) => {              addToMap(constructor, ctor); -        }, { withFields: Deserializable.withFields }); +        }, { withFields: (fields: string[]) => Deserializable.withFields(fields, name, afterDeserialize) });      }      addToMap(constructor.name, constructor);  }  export namespace Deserializable { -    export function withFields(fields: string[]) { +    export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) {          return function (constructor: { new(...fields: any[]): any }) { -            Deserializable(constructor); +            Deserializable(name || constructor.name, afterDeserialize)(constructor);              let schema = getDefaultModelSchema(constructor);              if (schema) {                  schema.factory = context => { @@ -135,6 +138,6 @@ export namespace Deserializable {  export function autoObject(): PropSchema {      return custom(          (s) => SerializationHelper.Serialize(s), -        (s) => SerializationHelper.Deserialize(s) +        (json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res))      );  }
\ No newline at end of file diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 1f95af00c..79a4e50d5 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -179,7 +179,7 @@ declare class Doc extends RefField {      // [ToScriptString](): string;  } -declare class ListImpl<T extends Field> extends ObjectField { +declare class List<T extends Field> extends ObjectField {      constructor(fields?: T[]);      [index: number]: T | (T extends RefField ? Promise<T> : never);      [Copy](): ObjectField; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2f7bea365..255855b45 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -304,7 +304,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          iconDoc.height = Number(MINIMIZED_ICON_SIZE);          iconDoc.x = NumCast(doc.x);          iconDoc.y = NumCast(doc.y) - 24; -        iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document.proto!)); +        iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));          selected.length === 1 && (doc.minimizedDoc = iconDoc);          selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);          return iconDoc; @@ -346,7 +346,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>      onRadiusMove = (e: PointerEvent): void => {          let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1])); -        SelectionManager.SelectedDocuments().map(dv => dv.props.Document.borderRounding = Doc.GetProto(dv.props.Document).borderRounding = `${Math.min(100, dist)}%`); +        SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)). +            map(d => d.borderRounding = `${Math.min(100, dist)}%`);          e.stopPropagation();          e.preventDefault();      } @@ -525,8 +526,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  let actualdH = Math.max(height + (dH * scale), 20);                  doc.x = (doc.x || 0) + dX * (actualdW - width);                  doc.y = (doc.y || 0) + dY * (actualdH - height); -                let proto = Doc.GetProto(element.props.Document); -                let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect, false) && nwidth && nheight); +                let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here... +                let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect) && nwidth && nheight);                  if (fixedAspect && (!nwidth || !nheight)) {                      proto.nativeWidth = nwidth = doc.width || 0;                      proto.nativeHeight = nheight = doc.height || 0; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index f2cdffd38..c66a92f48 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -93,7 +93,7 @@ export class EditableView extends React.Component<EditableProps> {                  <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}                      style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}                      onClick={this.onClick} > -                    <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span> +                    <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span>                  </div>              );          } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index e8a588e58..7477c5b4f 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,4 +1,4 @@ -import { UndoManager, undoBatch } from "../util/UndoManager"; +import { UndoManager } from "../util/UndoManager";  import { SelectionManager } from "../util/SelectionManager";  import { CollectionDockingView } from "./collections/CollectionDockingView";  import { MainView } from "./MainView"; @@ -144,9 +144,11 @@ export default class KeyManager {                  break;              case "y":                  UndoManager.Redo(); +                stopPropagation = false;                  break;              case "z":                  UndoManager.Undo(); +                stopPropagation = false;                  break;              case "a":              case "c": diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 3e0d7b476..c4cd863d1 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -6,7 +6,7 @@ import "./InkingCanvas.scss";  import { InkingControl } from "./InkingControl";  import { InkingStroke } from "./InkingStroke";  import React = require("react"); -import { undoBatch, UndoManager } from "../util/UndoManager"; +import { UndoManager } from "../util/UndoManager";  import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";  import { Doc } from "../../new_fields/Doc";  import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; @@ -178,7 +178,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {      render() {          let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None ? "canSelect" : "noSelect";          return ( -            <div className="inkingCanvas" > +            <div className="inkingCanvas">                  <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />                  {this.props.children()}                  {this.drawnPaths} diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index c7f7bdb66..58c83915b 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,5 +1,5 @@  import { observable, action, computed, runInAction } from "mobx"; -import { ColorResult } from 'react-color'; +import { ColorState } from 'react-color';  import React = require("react");  import { observer } from "mobx-react";  import "./InkingControl.scss"; @@ -20,7 +20,7 @@ export class InkingControl extends React.Component {      static Instance: InkingControl = new InkingControl({});      @observable private _selectedTool: InkTool = InkTool.None;      @observable private _selectedColor: string = "rgb(244, 67, 54)"; -    @observable private _selectedWidth: string = "25"; +    @observable private _selectedWidth: string = "5";      @observable public _open: boolean = false;      constructor(props: Readonly<{}>) { @@ -41,13 +41,13 @@ export class InkingControl extends React.Component {      }      @undoBatch -    switchColor = action((color: ColorResult): void => { +    switchColor = action((color: ColorState): void => {          this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");          if (InkingControl.Instance.selectedTool === InkTool.None) {              if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;              let selected = SelectionManager.SelectedDocuments();              let oldColors = selected.map(view => { -                let targetDoc = view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); +                let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);                  let oldColor = StrCast(targetDoc.backgroundColor);                  targetDoc.backgroundColor = this._selectedColor;                  return { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 94a4835a1..61a013963 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@  import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons';  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';  import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';  import { observer } from 'mobx-react'; @@ -131,6 +131,7 @@ export class MainView extends React.Component {          library.add(faArrowDown);          library.add(faArrowUp);          library.add(faCloudUploadAlt); +        library.add(faBolt);          this.initEventListeners();          this.initAuthenticationRouters();      } @@ -378,10 +379,12 @@ export class MainView extends React.Component {          let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));          let addTreeNode = action(() => CurrentUserUtils.UserDocument);          let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); +        let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" }));          let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));          let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [              [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], +            [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],              // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode],              [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode],          ]; @@ -394,6 +397,7 @@ export class MainView extends React.Component {              <div id="add-options-content">                  <ul id="add-options-list">                      <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li> +                    <li key="presentation"><button className="add-button round-button" title="Open Presentation View" onClick={() => PresentationView.Instance.toggle(undefined)}><FontAwesomeIcon icon="table" size="sm" /></button></li>                      <li key="undo"><button className="add-button round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>                      <li key="redo"><button className="add-button round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>                      {btns.map(btn => @@ -444,7 +448,6 @@ export class MainView extends React.Component {          this.isSearchVisible = !this.isSearchVisible;      } -      render() {          return (              <div id="main-div"> diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index 4d1e8cf0b..dc122497f 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -32,7 +32,7 @@  }  .overlayWindow-resizeDragger { -    background-color: red; +    background-color: rgb(0, 0, 0);      position: absolute;      right: 0px;      bottom: 0px; diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 6eabc7b70..9e538cf1b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -2,7 +2,7 @@ import * as React from 'react';  import { observer } from 'mobx-react';  import { observable, action } from 'mobx';  import './ScriptingRepl.scss'; -import { Scripting, CompileScript, ts } from '../util/Scripting'; +import { Scripting, CompileScript, ts, Transformer } from '../util/Scripting';  import { DocumentManager } from '../util/DocumentManager';  import { DocumentView } from './nodes/DocumentView';  import { OverlayView } from './OverlayView'; @@ -16,17 +16,16 @@ library.add(faCaretRight);  @observer  export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {      render() { -        this.props.view.props.ScreenToLocalTransform(); -        this.props.view.props.Document.width; -        this.props.view.props.Document.height; -        const screenCoords = this.props.view.screenRect(); +        const view = this.props.view; +        const transform = view.props.ScreenToLocalTransform().scale(view.props.ContentScaling()).inverse(); +        const { x, y, width, height } = transform.transformBounds(0, 0, view.props.PanelWidth(), view.props.PanelHeight());          return (              <div className="documentIcon-outerDiv" style={{                  position: "absolute", -                transform: `translate(${screenCoords.left + screenCoords.width / 2}px, ${screenCoords.top}px)`, +                transform: `translate(${x + width / 2}px, ${y}px)`,              }}> -                <p >${this.props.index}</p> +                <p>${this.props.index}</p>              </div>          );      } @@ -106,31 +105,35 @@ export class ScriptingRepl extends React.Component {      private args: any = {}; -    getTransformer: ts.TransformerFactory<ts.SourceFile> = context => { -        const knownVars: { [name: string]: number } = {}; -        const usedDocuments: number[] = []; -        Scripting.getGlobals().forEach(global => knownVars[global] = 1); -        return root => { -            function visit(node: ts.Node) { -                node = ts.visitEachChild(node, visit, context); +    getTransformer = (): Transformer => { +        return { +            transformer: context => { +                const knownVars: { [name: string]: number } = {}; +                const usedDocuments: number[] = []; +                Scripting.getGlobals().forEach(global => knownVars[global] = 1); +                return root => { +                    function visit(node: ts.Node) { +                        node = ts.visitEachChild(node, visit, context); -                if (ts.isIdentifier(node)) { -                    const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; -                    const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; -                    if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { -                        const match = node.text.match(/\$([0-9]+)/); -                        if (match) { -                            const m = parseInt(match[1]); -                            usedDocuments.push(m); -                        } else { -                            return ts.createPropertyAccess(ts.createIdentifier("args"), node); +                        if (ts.isIdentifier(node)) { +                            const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; +                            const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; +                            if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { +                                const match = node.text.match(/\$([0-9]+)/); +                                if (match) { +                                    const m = parseInt(match[1]); +                                    usedDocuments.push(m); +                                } else { +                                    return ts.createPropertyAccess(ts.createIdentifier("args"), node); +                                } +                            }                          } -                    } -                } -                return node; +                        return node; +                    } +                    return ts.visitNode(root, visit); +                };              } -            return ts.visitNode(root, visit);          };      } @@ -142,7 +145,7 @@ export class ScriptingRepl extends React.Component {                  const docGlobals: { [name: string]: any } = {};                  DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`$${i}`] = dv.props.Document);                  const globals = Scripting.makeMutableGlobalsCopy(docGlobals); -                const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer, globals }); +                const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals });                  if (!script.compiled) {                      return;                  } diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss index 34bcb705e..583e6f6ca 100644 --- a/src/client/views/collections/CollectionBaseView.scss +++ b/src/client/views/collections/CollectionBaseView.scss @@ -6,7 +6,7 @@      border-radius: 0 0 $border-radius $border-radius;      box-sizing: border-box;      border-radius: inherit; -    pointer-events: all;      width:100%;      height:100%; +    overflow: auto;  }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 72faf52c4..67112ae7c 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -124,9 +124,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {      @action.bound      moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {          let self = this; -        let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; +        let targetDataDoc = this.props.Document;          if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) { -            //if (Doc.AreProtosEqual(this.extensionDoc, targetCollection)) {              return true;          }          if (this.removeDocument(doc)) { @@ -146,7 +145,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {          const viewtype = this.collectionViewType;          return (              <div id="collectionBaseView" -                style={{ overflow: "auto", boxShadow: `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }} +                style={{ +                    pointerEvents: this.props.Document.isBackground ? "none" : "all", +                    boxShadow: `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` +                }}                  className={this.props.className || "collectionView-cont"}                  onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>                  {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 2cf50e551..119aa7c19 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -15,7 +15,7 @@ import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_field  import { Docs } from "../../documents/Documents";  import { Gateway } from "../../northstar/manager/Gateway";  import { SetupDrag, DragManager } from "../../util/DragManager"; -import { CompileScript } from "../../util/Scripting"; +import { CompileScript, ts, Transformer } from "../../util/Scripting";  import { Transform } from "../../util/Transform";  import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';  import { ContextMenu } from "../ContextMenu"; @@ -30,8 +30,6 @@ import { CollectionSubView } from "./CollectionSubView";  import { CollectionVideoView } from "./CollectionVideoView";  import { CollectionView } from "./CollectionView";  import { undoBatch } from "../../util/UndoManager"; -import { timesSeries } from "async"; -import { ImageBox } from "../nodes/ImageBox";  import { ComputedField } from "../../../new_fields/ScriptField"; @@ -99,6 +97,78 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {          return this.props.Document;      } +    getField(row: number, col?: number) { +        const docs = DocListCast(this.props.Document[this.props.fieldKey]); +        row = row % docs.length; +        while (row < 0) row += docs.length; +        const columns = this.columns; +        const doc = docs[row]; +        if (col === undefined) { +            return doc; +        } +        if (col >= 0 && col < columns.length) { +            const column = this.columns[col]; +            return doc[column]; +        } +        return undefined; +    } + +    createTransformer = (row: number, col: number): Transformer => { +        const self = this; +        const captures: { [name: string]: Field } = {}; + +        const transformer: ts.TransformerFactory<ts.SourceFile> = context => { +            return root => { +                function visit(node: ts.Node) { +                    node = ts.visitEachChild(node, visit, context); +                    if (ts.isIdentifier(node)) { +                        const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; +                        const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; +                        if (isntPropAccess && isntPropAssign) { +                            if (node.text === "$r") { +                                return ts.createNumericLiteral(row.toString()); +                            } else if (node.text === "$c") { +                                return ts.createNumericLiteral(col.toString()); +                            } else if (node.text === "$") { +                                if (ts.isCallExpression(node.parent)) { +                                    captures.doc = self.props.Document; +                                    captures.key = self.props.fieldKey; +                                } +                            } +                        } +                    } + +                    return node; +                } +                return ts.visitNode(root, visit); +            }; +        }; + +        const getVars = () => { +            return { capturedVariables: captures }; +        }; + +        return { transformer, getVars }; +    } + +    setComputed(script: string, doc: Doc, field: string, row: number, col: number): boolean { +        script = +            `const $ = (row:number, col?:number) => { +                if(col === undefined) { +                    return (doc as any)[key][row + ${row}]; +                } +                return (doc as any)[key][row + ${row}][(doc as any).schemaColumns[col + ${col}]]; +            } +            return ${script}`; +        const compiled = CompileScript(script, { params: { this: Doc.name }, typecheck: true, transformer: this.createTransformer(row, col) }); +        if (compiled.compiled) { +            doc[field] = new ComputedField(compiled); +            return true; +        } + +        return false; +    } +      renderCell = (rowProps: CellInfo) => {          let props: FieldViewProps = {              Document: rowProps.original, @@ -124,12 +194,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {              (!this.props.CollectionView.props.isSelected() ? undefined :                  SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));          }; -        let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { -            const res = run({ this: doc }); +        let applyToDoc = (doc: Doc, row: number, column: number, run: (args?: { [name: string]: any }) => any) => { +            const res = run({ this: doc, $r: row, $c: column, $: (r: number = 0, c: number = 0) => this.getField(r + row, c + column) });              if (!res.success) return false;              doc[props.fieldKey] = res.result;              return true;          }; +        const colIndex = this.columns.indexOf(rowProps.column.id!);          return (              <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}>                  <EditableView @@ -144,21 +215,23 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {                          return "";                      }}                      SetValue={(value: string) => { -                        let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } }); +                        if (value.startsWith(":=")) { +                            return this.setComputed(value.substring(2), props.Document, rowProps.column.id!, rowProps.index, colIndex); +                        } +                        let script = CompileScript(value, { addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });                          if (!script.compiled) {                              return false;                          } -                        return applyToDoc(props.Document, script.run); +                        return applyToDoc(props.Document, rowProps.index, colIndex, script.run);                      }}                      OnFillDown={async (value: string) => { -                        let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } }); +                        let script = CompileScript(value, { addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });                          if (!script.compiled) {                              return;                          }                          const run = script.run; -                        //TODO This should be able to be refactored to compile the script once                          const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); -                        val && val.forEach(doc => applyToDoc(doc, run)); +                        val && val.forEach((doc, i) => applyToDoc(doc, i, colIndex, run));                      }}>                  </EditableView>              </div > diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 0e5f9a321..5a123bf65 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react";  import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols";  import { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, Utils } from "../../../Utils"; +import { emptyFunction, Utils, returnTrue } from "../../../Utils";  import { CollectionSchemaPreview } from "./CollectionSchemaView";  import "./CollectionStackingView.scss";  import { CollectionSubView } from "./CollectionSubView"; @@ -23,15 +23,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {      _docXfs: any[] = [];      _columnStart: number = 0;      @observable private cursor: CursorProperty = "grab"; -    @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } -    @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } -    @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } -    @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } -    @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } +    @computed get xMargin() { return NumCast(this.layoutDoc.xMargin, 2 * this.gridGap); } +    @computed get yMargin() { return NumCast(this.layoutDoc.yMargin, 2 * this.gridGap); } +    @computed get gridGap() { return NumCast(this.layoutDoc.gridGap, 10); } +    @computed get singleColumn() { return BoolCast(this.layoutDoc.singleColumn, true); } +    @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.layoutDoc.columnWidth, 250)); }      @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } +    get layoutDoc() { +        // if this document's layout field contains a document (ie, a rendering template), then we will use that +        // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. +        return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; +    }      @computed get Sections() { -        let sectionFilter = StrCast(this.props.Document.sectionFilter); +        let sectionFilter = StrCast(this.layoutDoc.sectionFilter);          let fields = new Map<object, Doc[]>();          sectionFilter && this.filteredChildren.map(d => {              let sectionValue = (d[sectionFilter] ? d[sectionFilter] : "-undefined-") as object; @@ -42,10 +47,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {      }      componentDidMount() {          this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], -            () => this.singleColumn && -                (this.props.Document.height = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => -                    height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) -            , { fireImmediately: true }); +            () => { +                if (this.singleColumn) { +                    let hgt = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => { +                        let xhgt = height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap); +                        return xhgt; +                    }, this.yMargin); +                    this.layoutDoc.height = hgt; +                } +            }, { fireImmediately: true });      }      componentWillUnmount() {          this._heightDisposer && this._heightDisposer(); @@ -65,7 +75,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {      }      getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) { -        let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; +        let resolvedDataDoc = !this.layoutDoc.isTemplate && this.props.DataDoc !== this.layoutDoc ? this.props.DataDoc : undefined;          let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth;          let height = () => this.getDocHeight(layoutDoc);          let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); @@ -74,6 +84,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {              DataDocument={resolvedDataDoc}              showOverlays={this.overlays}              renderDepth={this.props.renderDepth} +            fitToBox={true}              width={width}              height={height}              getTransform={finalDxf} @@ -152,7 +163,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {          let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];          let delta = dragPos - this._columnStart;          this._columnStart = dragPos; -        this.props.Document.columnWidth = this.columnWidth + delta; +        this.layoutDoc.columnWidth = this.columnWidth + delta;      }      @action @@ -249,8 +260,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {                      ["width > height", this.filteredChildren.filter(f => f[WidthSym]() >= 1 + f[HeightSym]())],                      ["width = height", this.filteredChildren.filter(f => Math.abs(f[WidthSym]() - f[HeightSym]()) < 1)],                      ["height > width", this.filteredChildren.filter(f => f[WidthSym]() + 1 <= f[HeightSym]())]]. */} -                {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()). -                    map(section => this.section(section[0].toString(), section[1] as Doc[])) : +                {this.layoutDoc.sectionFilter ? Array.from(this.Sections.entries()). +                    map(section => this.section(section[0].toString(), section[1])) :                      this.section("", this.filteredChildren)}              </div>          ); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 55ba71722..26ebbfe63 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -112,7 +112,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {                  } else if (de.data.moveDocument) {                      let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;                      added = movedDocs.reduce((added: boolean, d) => -                        de.data.moveDocument(d, /*this.props.DataDoc ? this.props.DataDoc :*/ this.props.Document, this.props.addDocument) || added, false); +                        de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);                  } else {                      added = de.data.droppedDocuments.reduce((added: boolean, d) => {                          let moved = this.props.addDocument(d); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d05cc375e..a1697f9b4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -3,7 +3,7 @@ import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaret  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';  import { action, computed, observable, trace, untracked } from "mobx";  import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym, Opt, Field } from '../../../new_fields/Doc';  import { Id } from '../../../new_fields/FieldSymbols';  import { List } from '../../../new_fields/List';  import { Document, listSpec } from '../../../new_fields/Schema'; @@ -26,6 +26,8 @@ import { CollectionSubView } from "./CollectionSubView";  import "./CollectionTreeView.scss";  import React = require("react");  import { LinkManager } from '../../util/LinkManager'; +import { ComputedField } from '../../../new_fields/ScriptField'; +import { KeyValueBox } from '../nodes/KeyValueBox';  export interface TreeViewProps { @@ -68,33 +70,42 @@ class TreeView extends React.Component<TreeViewProps> {      private _header?: React.RefObject<HTMLDivElement> = React.createRef();      private _treedropDisposer?: DragManager.DragDropDisposer;      private _dref = React.createRef<HTMLDivElement>(); -    @observable __chosenKey: string = ""; -    @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; } +    @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, "data"); }      @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }      @observable _collapsed: boolean = true;      @computed get fieldKey() { -        let keys = Array.from(Object.keys(this.resolvedDataDoc));  // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set -        if (this.resolvedDataDoc.proto instanceof Doc) { -            let arr = Array.from(Object.keys(this.resolvedDataDoc.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set +        let target = this.props.document; +        let keys = Array.from(Object.keys(target));  // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set +        if (target.proto instanceof Doc) { +            let arr = Array.from(Object.keys(target.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set              keys.push(...arr);              while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);          }          let keyList: string[] = [];          keys.map(key => { -            let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc)); +            let docList = Cast(this.dataDoc[key], listSpec(Doc));              if (docList && docList.length > 0) {                  keyList.push(key);              }          });          let layout = StrCast(this.props.document.layout); -        if (layout.indexOf("fieldKey={\"") !== -1) { +        if (layout.indexOf("fieldKey={\"") !== -1 && layout.indexOf("fieldExt=") === -1) {              return layout.split("fieldKey={\"")[1].split("\"")[0];          }          return keyList.length ? keyList[0] : "data";      } -    @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; } +    @computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; } +    @computed get resolvedDataDoc() { +        if (this.props.dataDoc === undefined && this.props.document.layout instanceof Doc) { +            // if there is no dataDoc (ie, we're not rendering a template layout), but this document +            // has a template layout document, then we will render the template layout but use  +            // this document as the data document for the layout. +            return this.props.document; +        } +        return this.props.dataDoc ? this.props.dataDoc : undefined; +    }      protected createTreeDropTarget = (ele: HTMLDivElement) => {          this._treedropDisposer && this._treedropDisposer(); @@ -103,7 +114,7 @@ class TreeView extends React.Component<TreeViewProps> {          }      } -    @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); +    @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);      @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight");      onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); @@ -115,12 +126,12 @@ class TreeView extends React.Component<TreeViewProps> {          }      }      onPointerLeave = (e: React.PointerEvent): void => { -        this.props.document.libraryBrush = undefined; +        this.props.document.libraryBrush = false;          this._header!.current!.className = "treeViewItem-header";          document.removeEventListener("pointermove", this.onDragMove, true);      }      onDragMove = (e: PointerEvent): void => { -        this.props.document.libraryBrush = undefined; +        this.props.document.libraryBrush = false;          let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);          let rect = this._header!.current!.getBoundingClientRect();          let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); @@ -135,7 +146,7 @@ class TreeView extends React.Component<TreeViewProps> {      @action      remove = (document: Document, key: string): boolean => { -        let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []); +        let children = Cast(this.dataDoc[key], listSpec(Doc), []);          if (children.indexOf(document) !== -1) {              children.splice(children.indexOf(document), 1);              return true; @@ -151,8 +162,8 @@ class TreeView extends React.Component<TreeViewProps> {      indent = () => this.props.addDocument(this.props.document) && this.delete()      renderBullet() { -        let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc)); -        let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc); +        let docList = Cast(this.dataDoc[this.fieldKey], listSpec(Doc)); +        let doc = Cast(this.dataDoc[this.fieldKey], Doc);          let isDoc = doc instanceof Doc || docList;          let c;          return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> @@ -164,58 +175,41 @@ class TreeView extends React.Component<TreeViewProps> {      editableView = (key: string, style?: string) => (<EditableView          oneLine={true}          display={"inline"} -        editing={this.resolvedDataDoc[Id] === TreeView.loadId} +        editing={this.dataDoc[Id] === TreeView.loadId}          contents={StrCast(this.props.document[key])}          height={36}          fontStyle={style}          fontSize={12}          GetValue={() => StrCast(this.props.document[key])} -        SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true} +        SetValue={(value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true}          OnFillDown={(value: string) => { -            Doc.GetProto(this.resolvedDataDoc)[key] = value; -            let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); +            Doc.GetProto(this.dataDoc)[key] = value; +            let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined; +            if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });              TreeView.loadId = doc[Id];              return this.props.addDocument(doc);          }}          OnTab={() => this.props.indentDocument && this.props.indentDocument()}      />) -    @computed get keyList() { -        let keys = Array.from(Object.keys(this.resolvedDataDoc)); -        if (this.resolvedDataDoc.proto instanceof Doc) { -            keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); -        } -        let keyList: string[] = keys.reduce((l, key) => { -            let listspec = DocListCast(this.resolvedDataDoc[key]); -            if (listspec && listspec.length) return [...l, key]; -            return l; -        }, [] as string[]); -        keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key)); -        if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links"); -        if (keyList.indexOf(this.fieldKey) !== -1) { -            keyList.splice(keyList.indexOf(this.fieldKey), 1); -        } -        keyList.splice(0, 0, this.fieldKey); -        return keyList.filter((item, index) => keyList.indexOf(item) >= index); -    }      /**       * Renders the EditableView title element for placement into the tree.       */      renderTitle() {          let reference = React.createRef<HTMLDivElement>(); -        let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); +        let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);          let headerElements = ( -            <span className="collectionTreeView-keyHeader" key={this._chosenKey + "chosen"} +            <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}                  onPointerDown={action(() => { -                    let ind = this.keyList.indexOf(this._chosenKey); -                    ind = (ind + 1) % this.keyList.length; -                    this.__chosenKey = this.keyList[ind]; -                })} > -                {this._chosenKey} +                    this.props.document.treeViewExpandedView = this.treeViewExpandedView === "data" ? "fields" : +                        this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" : "data"; +                    this._collapsed = false; +                })}> +                {this.treeViewExpandedView}              </span>);          let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : []; -        let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : ( +        let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (              <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>                  <FontAwesomeIcon icon="angle-right" size="lg" />              </div>); @@ -237,16 +231,15 @@ class TreeView extends React.Component<TreeViewProps> {      onWorkspaceContextMenu = (e: React.MouseEvent): void => {          if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 -            ContextMenu.Instance.addItem({ description: (BoolCast(this.props.document.embed) ? "Collapse" : "Expand") + " inline", event: () => this.props.document.embed = !BoolCast(this.props.document.embed), icon: "expand" });              if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {                  ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" });                  ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); -                if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) { -                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" }); +                if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { +                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });                  }                  ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });              } else { -                ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" }); +                ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.dataDoc)), icon: "caret-square-right" });                  ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });              }              ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); @@ -274,7 +267,7 @@ class TreeView extends React.Component<TreeViewProps> {              if (de.data.draggedDocuments[0] === this.props.document) return true;              let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);              if (inside) { -                let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc)); +                let docList = Cast(this.dataDoc.data, listSpec(Doc));                  if (docList !== undefined) {                      addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };                  } @@ -299,8 +292,8 @@ class TreeView extends React.Component<TreeViewProps> {      renderLinks = () => {          let ele: JSX.Element[] = []; -        let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); -        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before); +        let remDoc = (doc: Doc) => this.remove(doc, this.fieldKey); +        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this.fieldKey, doc, addBefore, before);          let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document);          groups.forEach((groupLinkDocs, groupType) => {              // let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document)); @@ -326,7 +319,7 @@ class TreeView extends React.Component<TreeViewProps> {      @computed get boundsOfCollectionDocument() {          if (StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1) return undefined; -        let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); +        let layoutDoc = this.props.document;          return Doc.ComputeContentBounds(DocListCast(layoutDoc.data));      }      docWidth = () => { @@ -344,27 +337,75 @@ class TreeView extends React.Component<TreeViewProps> {          })());      } +    noOverlays = (doc: Doc) => ({ title: "", caption: "" }); + +    expandedField = (doc?: Doc) => { +        if (!doc) return <div />; +        let realDoc = doc; + +        let ids: { [key: string]: string } = {}; +        Object.keys(doc).forEach(key => { +            if (!(key in ids) && realDoc[key] !== ComputedField.undefined) { +                ids[key] = key; +            } +        }); + +        let rows: JSX.Element[] = []; +        for (let key of Object.keys(ids).sort()) { +            let contents = realDoc[key] ? realDoc[key] : undefined; +            let contentElement: JSX.Element[] | JSX.Element = []; + +            if (contents instanceof Doc || Cast(contents, listSpec(Doc))) { +                let docList = contents; +                let remDoc = (doc: Doc) => this.remove(doc, key); +                let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before); +                contentElement = key === "links" ? this.renderLinks() : +                    TreeView.GetChildElements(docList instanceof Doc ? [docList] : DocListCast(docList), this.props.treeViewId, realDoc, undefined, key, addDoc, remDoc, this.move, +                        this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth); +            } else { +                contentElement = <EditableView +                    key="editableView" +                    contents={contents ? contents.toString() : "null"} +                    height={13} +                    fontSize={12} +                    GetValue={() => Field.toKeyValueString(realDoc, key)} +                    SetValue={(value: string) => KeyValueBox.SetField(realDoc, key, value)} />; +            } +            rows.push(<div style={{ display: "flex" }} key={key}> +                <span style={{ fontWeight: "bold" }}>{key + ":"}</span> +                  +                {contentElement} +            </div>); +        } +        return rows; +    } +      render() {          let contentElement: (JSX.Element | null) = null; -        let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc)); -        let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); -        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before); -        let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc); +        let docList = Cast(this.dataDoc[this.fieldKey], listSpec(Doc)); +        let remDoc = (doc: Doc) => this.remove(doc, this.fieldKey); +        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc, addBefore, before);          if (!this._collapsed) { -            if (!this.props.document.embed) { -                contentElement = <ul key={this._chosenKey + "more"}> -                    {this._chosenKey === "links" ? this.renderLinks() : -                        TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move, +            if (this.treeViewExpandedView === "data") { +                let doc = Cast(this.props.document[this.fieldKey], Doc); +                contentElement = <ul key={this.fieldKey + "more"}> +                    {this.fieldKey === "links" ? this.renderLinks() : +                        TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.resolvedDataDoc, this.fieldKey, addDoc, remDoc, this.move,                              this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)}                  </ul >; +            } else if (this.treeViewExpandedView === "fields") { +                contentElement = <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> +                    {this.expandedField(this.dataDoc)} +                </div></ul>;              } else { -                let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); +                let layoutDoc = this.props.document;                  contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>                      <CollectionSchemaPreview                          Document={layoutDoc}                          DataDocument={this.resolvedDataDoc}                          renderDepth={this.props.renderDepth} +                        showOverlays={this.noOverlays}                          fitToBox={this.boundsOfCollectionDocument !== undefined}                          width={this.docWidth}                          height={this.docHeight} @@ -433,7 +474,7 @@ class TreeView extends React.Component<TreeViewProps> {                  dataDoc={dataDoc}                  containingCollection={containingCollection}                  treeViewId={treeViewId} -                key={child[Id] + "child " + i} +                key={child[Id]}                  indentDocument={indent}                  renderDepth={renderDepth}                  deleteDoc={remove} @@ -547,7 +588,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {                      SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}                      OnFillDown={(value: string) => {                          Doc.GetProto(this.props.Document).title = value; -                        let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); +                        let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined; +                        if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });                          TreeView.loadId = doc[Id];                          Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);                      }} /> diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 045c8531e..7781b26d9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,5 +1,5 @@  import { library } from '@fortawesome/fontawesome-svg-core'; -import { faProjectDiagram, faSignature, faColumns, faSquare, faTh, faImage, faThList, faTree, faEllipsisV } from '@fortawesome/free-solid-svg-icons'; +import { faProjectDiagram, faSignature, faColumns, faSquare, faTh, faImage, faThList, faTree, faEllipsisV, faFingerprint, faLaptopCode } from '@fortawesome/free-solid-svg-icons';  import { observer } from "mobx-react";  import * as React from 'react';  import { Doc, DocListCast, WidthSym, HeightSym } from '../../../new_fields/Doc'; @@ -25,6 +25,7 @@ library.add(faSquare);  library.add(faProjectDiagram);  library.add(faSignature);  library.add(faThList); +library.add(faFingerprint);  library.add(faColumns);  library.add(faEllipsisV);  library.add(faImage); @@ -50,7 +51,6 @@ export class CollectionView extends React.Component<FieldViewProps> {      get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } -    static _applyCount: number = 0;      onContextMenu = (e: React.MouseEvent): void => {          if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7              let subItems: ContextMenuProps[] = []; @@ -62,20 +62,14 @@ export class CollectionView extends React.Component<FieldViewProps> {              subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });              subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "ellipsis-v" });              subItems.push({ description: "Masonry", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Masonry), icon: "columns" }); +            switch (this.props.Document.viewType) { +                case CollectionViewType.Freeform: { +                    subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) }); +                    break; +                } +            }              ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems }); -            ContextMenu.Instance.addItem({ -                description: "Apply Template", event: undoBatch(() => { -                    let otherdoc = new Doc(); -                    otherdoc.width = this.props.Document[WidthSym](); -                    otherdoc.height = this.props.Document[HeightSym](); -                    otherdoc.title = this.props.Document.title + "(..." + CollectionView._applyCount++ + ")"; // previously "applied" -                    otherdoc.layout = Doc.MakeDelegate(this.props.Document); -                    otherdoc.miniLayout = StrCast(this.props.Document.miniLayout); -                    otherdoc.detailedLayout = otherdoc.layout; -                    otherdoc.type = DocumentType.TEMPLATE; -                    this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight"); -                }), icon: "project-diagram" -            }); +            ContextMenu.Instance.addItem({ description: "Apply Template", event: undoBatch(() => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight")), icon: "project-diagram" });          }      } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 00407d39a..cca199afa 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -19,6 +19,11 @@      transform-origin: left top;  } +.collectionFreeform-customText { +    position: absolute; +    text-align: center; +} +  .collectionfreeformview-container {      .collectionfreeformview>.jsx-parser {          position: inherit; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4a085bb70..bf938f433 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -32,7 +32,14 @@ import { ScriptField } from "../../../../new_fields/ScriptField";  import { OverlayView, OverlayElementOptions } from "../../OverlayView";  import { ScriptBox } from "../../ScriptBox";  import { CompileScript } from "../../../util/Scripting"; +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faEye } from "@fortawesome/free-regular-svg-icons"; +import { faTable, faPaintBrush, faAsterisk, faExpandArrowsAlt, faCompressArrowsAlt } from "@fortawesome/free-solid-svg-icons"; +import { undo } from "prosemirror-history"; +import { number } from "prop-types"; +library.add(faEye, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt);  export const panZoomSchema = createSchema({      panX: "number", @@ -52,25 +59,31 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {      private _lastY: number = 0;      private get _pwidth() { return this.props.PanelWidth(); }      private get _pheight() { return this.props.PanelHeight(); } +    private inkKey = "ink"; + +    get parentScaling() { +        return (this.props as any).ContentScaling && this.Document.nativeWidth && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1; +    }      @computed get contentBounds() { -        let bounds = this.props.fitToBox && !NumCast(this.nativeWidth) ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined; +        let bounds = this.fitToBox && !this.nativeWidth && !this.isAnnotationOverlay ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined;          return {              panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0,              panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, -            scale: bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1 +            scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling          };      } -    @computed get nativeWidth() { return this.Document.nativeWidth || 0; } -    @computed get nativeHeight() { return this.Document.nativeHeight || 0; } +    @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; } +    @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; } +    @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; }      public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')      private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }      private panX = () => this.contentBounds.panX;      private panY = () => this.contentBounds.panY;      private zoomScaling = () => this.contentBounds.scale; -    private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0;  // shift so pan position is at center of window for non-overlay collections -    private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections +    private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0;  // shift so pan position is at center of window for non-overlay collections +    private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections      private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());      private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);      private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -104,11 +117,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {      @undoBatch      @action      drop = (e: Event, de: DragManager.DropEvent) => { +        let xf = this.getTransform();          if (super.drop(e, de)) {              if (de.data instanceof DragManager.DocumentDragData) {                  if (de.data.droppedDocuments.length) { -                    let dragDoc = de.data.droppedDocuments[0]; -                    let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); +                    let [xp, yp] = xf.transformPoint(de.x, de.y);                      let x = xp - de.data.xOffset;                      let y = yp - de.data.yOffset;                      let dropX = NumCast(de.data.droppedDocuments[0].x); @@ -196,10 +209,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {                      this._pheight / this.zoomScaling());                  let panelwidth = panelDim[0];                  let panelheight = panelDim[1]; -                if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2; -                if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2; -                if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2; -                if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2; +                // if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2; +                // if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2; +                // if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2; +                // if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;              }              this.setPan(x - dx, y - dy);              this._lastX = e.pageX; @@ -359,7 +372,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {      getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {          let self = this;          let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; -        let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc); +        let layoutDoc = childDocLayout; +        if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) { +            Doc.UpdateDocumentExtensionForField(resolvedDataDoc, this.props.fieldKey); +            let fieldExtensionDoc = Doc.resolvedFieldDataDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy"); +            layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined); +        } else layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);          return {              DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined,              Document: layoutDoc, @@ -414,6 +432,25 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {          return result.result === undefined ? {} : result.result;      } +    private viewDefToJSX(viewDef: any): JSX.Element | undefined { +        if (viewDef.type === "text") { +            const text = Cast(viewDef.text, "string"); +            const x = Cast(viewDef.x, "number"); +            const y = Cast(viewDef.y, "number"); +            const width = Cast(viewDef.width, "number"); +            const height = Cast(viewDef.height, "number"); +            const fontSize = Cast(viewDef.fontSize, "number"); +            if ([text, x, y].some(val => val === undefined)) { +                return undefined; +            } + +            return <div className="collectionFreeform-customText" style={{ +                transform: `translate(${x}px, ${y}px)`, +                width, height, fontSize +            }}>{text}</div>; +        } +    } +      @computed.struct      get views() {          let curPage = FieldValue(this.Document.curPage, -1); @@ -421,10 +458,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {          const script = this.Document.arrangeScript;          let state: any = undefined;          const docs = this.childDocs; +        let elements: JSX.Element[] = [];          if (initScript) {              const initResult = initScript.script.run({ docs, collection: this.Document });              if (initResult.success) { -                state = initResult.result; +                const result = initResult.result; +                const { state: scriptState, views } = result; +                state = scriptState; +                if (Array.isArray(views)) { +                    elements = views.reduce<JSX.Element[]>((prev, ele) => { +                        const jsx = this.viewDefToJSX(ele); +                        jsx && prev.push(jsx); +                        return prev; +                    }, elements); +                }              }          }          let docviews = docs.reduce((prev, doc) => { @@ -439,7 +486,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {                  }              }              return prev; -        }, [] as JSX.Element[]); +        }, elements);          setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... @@ -453,7 +500,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {      onContextMenu = () => {          ContextMenu.Instance.addItem({ +            description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, +            event: undoBatch(async () => this.props.Document.fitToBox = !this.fitToBox), +            icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" +        }); +        ContextMenu.Instance.addItem({              description: "Arrange contents in grid", +            icon: "table",              event: async () => {                  const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);                  UndoManager.RunInBatch(() => { @@ -479,42 +532,51 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {              }          });          ContextMenu.Instance.addItem({ -            description: "Add freeform arrangement", -            event: () => { -                let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { -                    let overlayDisposer: () => void = emptyFunction; -                    const script = this.Document[key]; -                    let originalText: string | undefined = undefined; -                    if (script) originalText = script.script.originalScript; -                    // tslint:disable-next-line: no-unnecessary-callback-wrapper -                    let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { -                        const script = CompileScript(text, { -                            params, -                            requiredType, -                            typecheck: false -                        }); -                        if (!script.compiled) { -                            onError(script.errors.map(error => error.messageText).join("\n")); -                            return; -                        } -                        const docs = DocListCast(this.Document[this.props.fieldKey]); -                        docs.map(d => d.transition = "transform 1s"); -                        this.Document[key] = new ScriptField(script); -                        overlayDisposer(); -                        setTimeout(() => docs.map(d => d.transition = undefined), 1200); -                    }} />; -                    overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); -                }; -                addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); -                addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); -            } +            description: "Analyze Strokes", event: async () => { +                let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); +                if (!data) { +                    return; +                } +                let relevantKeys = ["inkAnalysis", "handwriting"]; +                CognitiveServices.Inking.Manager.analyzer(this.fieldExtensionDoc, relevantKeys, data.inkData); +            }, icon: "paint-brush"          });      } +      private childViews = () => [          <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,          ...this.views      ] + +    public static AddCustomLayout(doc: Doc, dataKey: string): () => void { +        return () => { +            let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { +                let overlayDisposer: () => void = emptyFunction; +                const script = Cast(doc[key], ScriptField); +                let originalText: string | undefined = undefined; +                if (script) originalText = script.script.originalScript; +                // tslint:disable-next-line: no-unnecessary-callback-wrapper +                let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { +                    const script = CompileScript(text, { +                        params, +                        requiredType, +                        typecheck: false +                    }); +                    if (!script.compiled) { +                        onError(script.errors.map(error => error.messageText).join("\n")); +                        return; +                    } +                    doc[key] = new ScriptField(script); +                    overlayDisposer(); +                }} />; +                overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); +            }; +            addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); +            addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); +        }; +    } +      render() {          const easing = () => this.props.Document.panTransformType === "Ease"; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b765517a2..d96e93aeb 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -365,7 +365,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>      marqueeSelect() {          let selRect = this.Bounds;          let selection: Doc[] = []; -        this.props.activeDocuments().map(doc => { +        this.props.activeDocuments().filter(doc => !doc.isBackground).map(doc => {              var z = NumCast(doc.zoomBasis, 1);              var x = NumCast(doc.x);              var y = NumCast(doc.y); diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss new file mode 100644 index 000000000..92beafa15 --- /dev/null +++ b/src/client/views/nodes/ButtonBox.scss @@ -0,0 +1,12 @@ +.buttonBox-outerDiv { +    width: 100%; +    height: 100%; +    pointer-events: all; +    border-radius: inherit; +} + +.buttonBox-mainButton { +    width: 100%; +    height: 100%; +    border-radius: inherit; +}
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx new file mode 100644 index 000000000..744611661 --- /dev/null +++ b/src/client/views/nodes/ButtonBox.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { FieldViewProps, FieldView } from './FieldView'; +import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { ScriptField } from '../../../new_fields/ScriptField'; +import { DocComponent } from '../DocComponent'; +import { ContextMenu } from '../ContextMenu'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit } from '@fortawesome/free-regular-svg-icons'; +import { emptyFunction } from '../../../Utils'; +import { ScriptBox } from '../ScriptBox'; +import { CompileScript } from '../../util/Scripting'; +import { OverlayView } from '../OverlayView'; +import { Doc } from '../../../new_fields/Doc'; + +import './ButtonBox.scss'; +import { observer } from 'mobx-react'; + +library.add(faEdit); + +const ButtonSchema = createSchema({ +    onClick: ScriptField, +    text: "string" +}); + +type ButtonDocument = makeInterface<[typeof ButtonSchema]>; +const ButtonDocument = makeInterface(ButtonSchema); + +@observer +export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) { +    public static LayoutString() { return FieldView.LayoutString(ButtonBox); } + +    onClick = (e: React.MouseEvent) => { +        const onClick = this.Document.onClick; +        if (!onClick) { +            return; +        } +        e.stopPropagation(); +        e.preventDefault(); +        onClick.script.run({ this: this.props.Document }); +    } + +    onContextMenu = () => { +        ContextMenu.Instance.addItem({ +            description: "Edit OnClick script", icon: "edit", event: () => { +                let overlayDisposer: () => void = emptyFunction; +                const script = this.Document.onClick; +                let originalText: string | undefined = undefined; +                if (script) originalText = script.script.originalScript; +                // tslint:disable-next-line: no-unnecessary-callback-wrapper +                let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { +                    const script = CompileScript(text, { +                        params: { this: Doc.name }, +                        typecheck: false, +                        editable: true +                    }); +                    if (!script.compiled) { +                        onError(script.errors.map(error => error.messageText).join("\n")); +                        return; +                    } +                    this.Document.onClick = new ScriptField(script); +                    overlayDisposer(); +                }} />; +                overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnClick` }); +            } +        }); +    } + +    render() { +        return ( +            <div className="buttonBox-outerDiv" onContextMenu={this.onContextMenu}> +                <button className="buttonBox-mainButton" onClick={this.onClick}>{this.Document.text || "Button"}</button> +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index b09538d1a..389b9f68b 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -82,6 +82,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF      }      render() { +        const hasPosition = this.props.x !== undefined || this.props.y !== undefined;          return (              <div className="collectionFreeFormDocumentView-container"                  style={{ @@ -90,7 +91,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF                      backgroundColor: "transparent",                      borderRadius: this.borderRounding(),                      transform: this.transform, -                    transition: StrCast(this.props.Document.transition), +                    transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition),                      width: this.width,                      height: this.height,                      zIndex: this.Document.zIndex || 0, diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index ed6b224a7..91d4fb524 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -11,6 +11,7 @@ import { DocumentViewProps } from "./DocumentView";  import "./DocumentView.scss";  import { FormattedTextBox } from "./FormattedTextBox";  import { ImageBox } from "./ImageBox"; +import { ButtonBox } from "./ButtonBox";  import { IconBox } from "./IconBox";  import { KeyValueBox } from "./KeyValueBox";  import { PDFBox } from "./PDFBox"; @@ -64,7 +65,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {      get dataDoc() {          if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) { -            // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document +            // if there is no dataDoc (ie, we're not rendering a template layout), but this document              // has a template layout document, then we will render the template layout but use               // this document as the data document for the layout.              return this.props.Document; @@ -97,7 +98,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {          if (this.props.renderDepth > 7) return (null);          if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);          return <ObserverJsxParser -            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} +            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}              bindings={this.CreateBindings()}              jsx={this.finalLayout}              showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 3a4b46b7e..7c72fb6e6 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -4,7 +4,6 @@    position: inherit;    top: 0;    left:0; -  pointer-events: all;   // background: $light-color; //overflow: hidden;    transform-origin: left top; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 907ba3713..bae0b5b96 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -37,6 +37,7 @@ import { RouteStore } from '../../../server/RouteStore';  import { FormattedTextBox } from './FormattedTextBox';  import { OverlayView } from '../OverlayView';  import { ScriptingRepl } from '../ScriptingRepl'; +import { ClientUtils } from '../../util/ClientUtils';  import { EditableView } from '../EditableView';  const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -59,6 +60,7 @@ library.add(fa.faCrosshairs);  library.add(fa.faDesktop);  library.add(fa.faUnlock);  library.add(fa.faLock); +library.add(fa.faLaptopCode);  // const linkSchema = createSchema({ @@ -193,10 +195,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                  DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb);              }              else { -                Doc.GetProto(doc).isMinimized = !maximizing; -                Doc.GetProto(doc).isIconAnimating = undefined; +                doc.isMinimized = !maximizing; +                doc.isIconAnimating = undefined;              } -            Doc.GetProto(doc).willMaximize = false; +            doc.willMaximize = false;          },              2);      } @@ -295,9 +297,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu          if (this._doubleTap && this.props.renderDepth) {              let fullScreenAlias = Doc.MakeAlias(this.props.Document);              fullScreenAlias.templates = new List<string>(); +            Doc.UseDetailLayout(fullScreenAlias); +            fullScreenAlias.showCaption = true;              this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");              SelectionManager.DeselectAll(); -            this.props.Document.libraryBrush = undefined; +            this.props.Document.libraryBrush = false;          }          else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&              (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && @@ -316,20 +320,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                  expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;                  // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];                  if (expandedDocs.length) {   // bcz: need a better way to associate behaviors with click events on widget-documents -                    let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc));                      let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");                      let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);                      if (altKey || ctrlKey) {                          maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"));                          if (!maxLocation || maxLocation === "inPlace") { -                            let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); +                            let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);                              let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false);                              expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false); -                            let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); +                            let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);                              if (!hasView) {                                  this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));                              } -                            expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized); +                            expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);                          }                      }                      if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) { @@ -341,7 +344,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                          }                      } else {                          let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); -                        this.collapseTargetsToPoint(scrpt, expandedProtoDocs); +                        this.collapseTargetsToPoint(scrpt, expandedDocs);                      }                  }                  else if (linkedDocs.length) { @@ -503,7 +506,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu      @undoBatch      @action      freezeNativeDimensions = (): void => { -        let proto = Doc.GetProto(this.props.Document); +        let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);          if (proto.ignoreAspect === undefined && !proto.nativeWidth) {              proto.nativeWidth = this.props.PanelWidth();              proto.nativeHeight = this.props.PanelHeight(); @@ -511,6 +514,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu          }          proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false);      } +    @undoBatch +    @action +    makeBackground = (): void => { +        this.props.Document.isBackground = true; +    }      @undoBatch      @action @@ -541,6 +549,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu          cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });          cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });          cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); +        cm.addItem({ description: "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });          cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });          cm.addItem({              description: "Make Portal", event: () => { @@ -553,20 +562,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                  this.props.removeDocument && this.props.removeDocument(this.props.Document);              }, icon: "window-restore"          }); -        cm.addItem({ -            description: "Find aliases", event: async () => { -                const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); -                this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? -            }, icon: "search" -        }); +        // cm.addItem({ +        //     description: "Find aliases", event: async () => { +        //         const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); +        //         this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? +        //     }, icon: "search" +        // });          if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {              cm.addItem({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });          } -        cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); +        cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });          cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });          cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); -        cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); -        cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); +        if (!ClientUtils.RELEASE) { +            cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); +            cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); +        }          cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });          type User = { email: string, userDocumentId: string };          let usersMenu: ContextMenuProps[] = []; @@ -608,7 +619,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu      }      onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; -    onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = undefined; }; +    onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };      isSelected = () => SelectionManager.IsSelected(this);      @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; @@ -619,36 +630,42 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu          return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} DataDoc={this.dataDoc} />);      } +    get layoutDoc() { +        // if this document's layout field contains a document (ie, a rendering template), then we will use that +        // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. +        return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; +    }      render() {          if (this.Document.hidden) {              return null;          }          let self = this; -        let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor; -        let foregroundColor = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.color : this.props.Document.color); +        let backgroundColor = StrCast(this.layoutDoc.backgroundColor); +        let foregroundColor = StrCast(this.layoutDoc.color);          var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";          var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; -        let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.props.Document) : undefined; -        let showTitle = showOverlays && showOverlays.title ? showOverlays.title : StrCast(this.props.Document.showTitle); -        let showCaption = showOverlays && showOverlays.caption ? showOverlays.caption : StrCast(this.props.Document.showCaption); -        let templates = Cast(this.props.Document.templates, listSpec("string")); -        if (templates instanceof List) { +        let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; +        let showTitle = showOverlays && showOverlays.title !== "undefined" ? showOverlays.title : StrCast(this.layoutDoc.showTitle); +        let showCaption = showOverlays && showOverlays.caption !== "undefined" ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); +        let templates = Cast(this.layoutDoc.templates, listSpec("string")); +        if (!showOverlays && templates instanceof List) {              templates.map(str => {                  if (str.indexOf("{props.Document.title}") !== -1) showTitle = "title";                  if (str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption";              });          } -        let showTextTitle = showTitle && StrCast(this.props.Document.layout).startsWith("<FormattedTextBox") || (this.props.Document.layout instanceof Doc && StrCast(this.props.Document.layout.layout).startsWith("<FormattedTextBox")) ? showTitle : undefined; +        let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;          return (              <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}                  ref={this._mainCont}                  style={{ +                    pointerEvents: this.layoutDoc.isBackground ? "none" : "all",                      color: foregroundColor,                      outlineColor: "maroon",                      outlineStyle: "dashed", -                    outlineWidth: BoolCast(this.props.Document.libraryBrush) && !StrCast(this.props.Document.borderRounding) ? +                    outlineWidth: BoolCast(this.layoutDoc.libraryBrush) && !StrCast(Doc.GetProto(this.props.Document).borderRounding) ?                          `${this.props.ScreenToLocalTransform().Scale}px` : "0px", -                    border: BoolCast(this.props.Document.libraryBrush) && StrCast(this.props.Document.borderRounding) ? +                    border: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ?                          `dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined,                      borderRadius: "inherit",                      background: backgroundColor, @@ -663,22 +680,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                  {!showTitle && !showCaption ? this.contents :                      <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}> -                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "25px" : undefined }}> +                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 33px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>                              {this.contents}                          </div>                          {!showTitle ? (null) :                              <div style={{ -                                position: showTextTitle ? "relative" : "absolute", top: 0, textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre", -                                pointerEvents: "all", +                                position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre", +                                pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",                                  overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",                                  transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`                              }}>                                  <EditableView -                                    contents={this.props.Document[showTitle]} +                                    contents={this.layoutDoc[showTitle]}                                      display={"block"}                                      height={72} -                                    GetValue={() => StrCast(this.props.Document[showTitle])} -                                    SetValue={(value: string) => (Doc.GetProto(this.props.Document)[showTitle] = value) ? true : true} +                                    fontSize={12} +                                    GetValue={() => StrCast(this.layoutDoc[showTitle!])} +                                    SetValue={(value: string) => (Doc.GetProto(this.layoutDoc)[showTitle!] = value) ? true : true}                                  />                              </div>                          } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ea6730cd0..ffaee8042 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -87,7 +87,8 @@ export class FieldView extends React.Component<FieldViewProps> {              return <p>{field.date.toLocaleString()}</p>;          }          else if (field instanceof Doc) { -            return <p><b>{field.title + " : id= " + field[Id]}</b></p>; +            return <p><b>{field.title}</b></p>; +            //return <p><b>{field.title + " : id= " + field[Id]}</b></p>;              // let returnHundred = () => 100;              // return (              //     <DocumentContentsView Document={field} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0f60bd0fb..a36885616 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,5 +1,5 @@  import { library } from '@fortawesome/fontawesome-svg-core'; -import { faImage, faFileAudio } from '@fortawesome/free-solid-svg-icons'; +import { faImage, faFileAudio, faPaintBrush, faAsterisk } from '@fortawesome/free-solid-svg-icons';  import { action, observable, computed, runInAction } from 'mobx';  import { observer } from "mobx-react";  import Lightbox from 'react-image-lightbox'; @@ -27,13 +27,14 @@ import { Font } from '@react-pdf/renderer';  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';  import { CognitiveServices } from '../../cognitive_services/CognitiveServices';  import FaceRectangles from './FaceRectangles'; +import { faEye } from '@fortawesome/free-regular-svg-icons';  var requestImageSize = require('../../util/request-image-size');  var path = require('path');  const { Howl, Howler } = require('howler'); -library.add(faImage); -library.add(faFileAudio); +library.add(faImage, faEye, faPaintBrush); +library.add(faFileAudio, faAsterisk);  export const pageSchema = createSchema({ @@ -103,8 +104,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD                              e.stopPropagation();                          }                      } -                } else if (!this.props.isSelected()) { -                    e.stopPropagation();                  }              }));              // de.data.removeDocument()  bcz: need to implement @@ -175,7 +174,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD                  const url = Utils.prepend(files[0]);                  // upload to server with known URL                   let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 }); -                audioDoc.embed = true; +                audioDoc.treeViewExpandedView = "layout";                  let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));                  if (audioAnnos === undefined) {                      self.extensionDoc.audioAnnotations = new List([audioDoc]); @@ -216,12 +215,12 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD              });              let modes: ContextMenuProps[] = []; -            let dataDoc = Doc.GetProto(this.Document); +            let dataDoc = Doc.GetProto(this.props.Document);              modes.push({ description: "Generate Tags", event: () => CognitiveServices.Image.generateMetadata(dataDoc), icon: "tag" });              modes.push({ description: "Find Faces", event: () => CognitiveServices.Image.extractFaces(dataDoc), icon: "camera" }); -            ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs }); -            ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes }); +            ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); +            ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" });          }      } @@ -351,11 +350,11 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD          if (field instanceof ImageField) paths = [this.choosePath(field.url)];          paths.push(...altpaths);          // } -        let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; +        let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";          let rotation = NumCast(this.dataDoc.rotation, 0);          let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;          let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; -        let srcpath = paths[Math.min(paths.length, this.Document.curPage || 0)]; +        let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)];          if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index f80840f96..f2fef7f16 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -31,6 +31,8 @@ export interface PresViewProps {      Documents: List<Doc>;  } +const expandedWidth = 400; +  @observer  export class PresentationView extends React.Component<PresViewProps>  {      public static Instance: PresentationView; @@ -61,12 +63,25 @@ export class PresentationView extends React.Component<PresViewProps>  {      @observable titleInputElement: HTMLInputElement | undefined;      @observable PresTitleChangeOpen: boolean = false; +    @observable opacity = 1; +    @observable persistOpacity = true; +    @observable labelOpacity = 0; +      //initilize class variables      constructor(props: PresViewProps) {          super(props);          PresentationView.Instance = this;      } +    @action +    toggle = (forcedValue: boolean | undefined) => { +        if (forcedValue !== undefined) { +            this.curPresentation.width = forcedValue ? expandedWidth : 0; +        } else { +            this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth; +        } +    } +      //The first lifecycle function that gets called to set up the current presentation.      async componentWillMount() {          this.props.Documents.forEach(async (doc, index: number) => { @@ -543,7 +558,7 @@ export class PresentationView extends React.Component<PresViewProps>  {              this.curPresentation.data = new List([doc]);          } -        this.curPresentation.width = 400; +        this.toggle(true);      }      //Function that sets the store of the children docs. @@ -800,7 +815,7 @@ export class PresentationView extends React.Component<PresViewProps>  {          let width = NumCast(this.curPresentation.width);          return ( -            <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}> +            <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflow: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}>                  <div className="presentationView-heading">                      {this.renderSelectOrPresSelection()}                      <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> @@ -819,6 +834,18 @@ export class PresentationView extends React.Component<PresViewProps>  {                      {this.renderPlayPauseButton()}                      <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>                  </div> +                <input +                    type="checkbox" +                    onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { +                        this.persistOpacity = e.target.checked; +                        this.opacity = this.persistOpacity ? 1 : 0.4; +                    })} +                    checked={this.persistOpacity} +                    style={{ position: "absolute", bottom: 5, left: 5 }} +                    onPointerEnter={action(() => this.labelOpacity = 1)} +                    onPointerLeave={action(() => this.labelOpacity = 0)} +                /> +                <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>                  <PresentationViewList                      mainDocument={this.curPresentation}                      deleteDocument={this.RemoveDoc} diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index a995140e2..5c2ced2eb 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -205,13 +205,13 @@ export class SearchItem extends React.Component<SearchItemProps> {                  let doc1 = Cast(this.props.doc.anchor1, Doc, null);                  let doc2 = Cast(this.props.doc.anchor2, Doc, null); -                doc1 && (doc1.libraryBrush = undefined); -                doc2 && (doc2.libraryBrush = undefined); +                doc1 && (doc1.libraryBrush = false); +                doc2 && (doc2.libraryBrush = false);              }          } else {              let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);              docViews.forEach(element => { -                element.props.Document.libraryBrush = undefined; +                element.props.Document.libraryBrush = false;              });          }      } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 2ad6ae5f0..1a00db1c1 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -1,6 +1,6 @@  import { observable, action } from "mobx"; -import { serializable, primitive, map, alias, list } from "serializr"; -import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; +import { serializable, primitive, map, alias, list, PropSchema, custom } from "serializr"; +import { autoObject, SerializationHelper, Deserializable, afterDocDeserialize } from "../client/util/SerializationHelper";  import { DocServer } from "../client/DocServer";  import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";  import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types"; @@ -68,8 +68,17 @@ export function DocListCast(field: FieldResult): Doc[] {  export const WidthSym = Symbol("Width");  export const HeightSym = Symbol("Height"); +function fetchProto(doc: Doc) { +    const proto = doc.proto; +    if (proto instanceof Promise) { +        return proto; +    } +} + +let updatingFromServer = false; +  @scriptingGlobal -@Deserializable("doc").withFields(["id"]) +@Deserializable("Doc", fetchProto).withFields(["id"])  export class Doc extends RefField {      constructor(id?: FieldId, forceSave?: boolean) {          super(id); @@ -102,7 +111,7 @@ export class Doc extends RefField {      proto: Opt<Doc>;      [key: string]: FieldResult; -    @serializable(alias("fields", map(autoObject()))) +    @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize })))      private get __fields() {          return this.___fields;      } @@ -122,6 +131,9 @@ export class Doc extends RefField {      private ___fields: any = {};      private [Update] = (diff: any) => { +        if (updatingFromServer) { +            return; +        }          DocServer.UpdateField(this[Id], diff);      } @@ -134,16 +146,18 @@ export class Doc extends RefField {          return "invalid";      } -    public [HandleUpdate](diff: any) { +    public async [HandleUpdate](diff: any) {          const set = diff.$set;          if (set) {              for (const key in set) {                  if (!key.startsWith("fields.")) {                      continue;                  } -                const value = SerializationHelper.Deserialize(set[key]); +                const value = await SerializationHelper.Deserialize(set[key]);                  const fKey = key.substring(7); +                updatingFromServer = true;                  this[fKey] = value; +                updatingFromServer = false;              }          }          const unset = diff.$unset; @@ -153,7 +167,9 @@ export class Doc extends RefField {                      continue;                  }                  const fKey = key.substring(7); +                updatingFromServer = true;                  delete this[fKey]; +                updatingFromServer = false;              }          }      } @@ -244,7 +260,7 @@ export namespace Doc {          let r = (doc === other);          let r2 = (doc.proto === other);          let r3 = (other.proto === doc); -        let r4 = (doc.proto === other.proto); +        let r4 = (doc.proto === other.proto && other.proto !== undefined);          return r || r2 || r3 || r4;      } @@ -298,7 +314,7 @@ export namespace Doc {                  x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),                  r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)              }; -        }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); +        }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });          return bounds;      } @@ -314,20 +330,22 @@ export namespace Doc {      }      export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) { -        let extensionDoc = doc[fieldKey + "_ext"]; -        if (extensionDoc === undefined) { +        let docExtensionForField = doc[fieldKey + "_ext"] as Doc; +        if (docExtensionForField === undefined) {              setTimeout(() => { -                let docExtensionForField = new Doc(doc[Id] + fieldKey, true); -                docExtensionForField.title = doc.title + ":" + fieldKey + ".ext"; -                docExtensionForField.extendsDoc = doc; +                docExtensionForField = new Doc(doc[Id] + fieldKey, true); +                docExtensionForField.title = fieldKey + ".ext"; +                docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends. +                docExtensionForField.type = DocumentType.EXTENSION;                  let proto: Doc | undefined = doc;                  while (proto && !Doc.IsPrototype(proto)) {                      proto = proto.proto;                  }                  (proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField;              }, 0); -        } else if (extensionDoc instanceof Doc && extensionDoc.extendsDoc === undefined) { -            setTimeout(() => (extensionDoc as Doc).extendsDoc = doc, 0); +        } else if (doc instanceof Doc) { // backward compatibility -- add fields for docs that don't have them already +            docExtensionForField.extendsDoc === undefined && setTimeout(() => docExtensionForField.extendsDoc = doc, 0); +            docExtensionForField.type === undefined && setTimeout(() => docExtensionForField.type = DocumentType.EXTENSION, 0);          }      }      export function MakeAlias(doc: Doc) { @@ -337,11 +355,25 @@ export namespace Doc {          return Doc.MakeDelegate(doc); // bcz?      } +    // +    // Determines whether the combination of the layoutDoc and dataDoc represents +    // a template relationship.  If so, the layoutDoc will be expanded into a new +    // document that inherits the properties of the original layout while allowing +    // for individual layout properties to be overridden in the expanded layout. +    // +    export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) { +        return BoolCast(layoutDoc.isTemplate) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc); +    } + +    // +    // Returns an expanded template layout for a target data document. +    // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc +    // using the template layout doc's id as the field key. +    // If it doesn't find the expanded layout, then it makes a delegate of the template layout and +    // saves it on the data doc indexed by the template layout's id +    //      export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) { -        let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined; -        if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) { -            return templateLayoutDoc; -        } +        if (!WillExpandTemplateLayout(templateLayoutDoc, dataDoc) || !dataDoc) return templateLayoutDoc;          // if we have a data doc that doesn't match the layout, then we're rendering a template.          // ... which means we change the layout to be an expanded view of the template layout.            // This allows the view override the template's properties and be referenceable as its own document. @@ -350,19 +382,18 @@ export namespace Doc {          if (expandedTemplateLayout instanceof Doc) {              return expandedTemplateLayout;          } -        if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) { -            setTimeout(() => { -                let expandedDoc = Doc.MakeDelegate(templateLayoutDoc); -                expandedDoc.title = templateLayoutDoc.title + "[" + StrCast(dataDoc.title).match(/\.\.\.[0-9]*/) + "]"; -                expandedDoc.isExpandedTemplate = templateLayoutDoc; -                dataDoc[templateLayoutDoc[Id]] = expandedDoc; -            }, 0); +        let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]"; +        expandedTemplateLayout = dataDoc[expandedLayoutFieldKey]; +        if (expandedTemplateLayout instanceof Doc) { +            return expandedTemplateLayout; +        } +        if (expandedTemplateLayout === undefined) { +            setTimeout(() => +                dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "["+templateLayoutDoc.title + "]"), 0);          }          return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending.      } -    let _pendingExpansions: Map<string, boolean> = new Map(); -      export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {          const copy = new Doc;          Object.keys(doc).forEach(key => { @@ -387,17 +418,32 @@ export namespace Doc {          return copy;      } -    export function MakeDelegate(doc: Doc, id?: string): Doc; -    export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>; -    export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> { +    export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc; +    export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>; +    export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> {          if (doc) {              const delegate = new Doc(id, true);              delegate.proto = doc; +            title && (delegate.title = title);              return delegate;          }          return undefined;      } +    let _applyCount: number = 0; +    export function ApplyTemplate(templateDoc: Doc) { +        if (!templateDoc) return undefined; +        let otherdoc = new Doc(); +        otherdoc.width = templateDoc[WidthSym](); +        otherdoc.height = templateDoc[HeightSym](); +        otherdoc.title = templateDoc.title + "(..." + _applyCount++ + ")"; +        otherdoc.layout = Doc.MakeDelegate(templateDoc); +        otherdoc.miniLayout = StrCast(templateDoc.miniLayout); +        otherdoc.detailedLayout = otherdoc.layout; +        otherdoc.type = DocumentType.TEMPLATE; +        return otherdoc; +    } +      export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {          // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)          let backgroundLayout = StrCast(fieldTemplate.backgroundLayout); @@ -407,7 +453,6 @@ export namespace Doc {          }          let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);          if (backgroundLayout) { -            layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);              backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);          }          let nw = Cast(fieldTemplate.nativeWidth, "number"); @@ -417,6 +462,7 @@ export namespace Doc {          layoutDelegate.layout = layout;          fieldTemplate.title = metaKey; +        fieldTemplate.templateField = metaKey;          fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;          fieldTemplate.backgroundLayout = backgroundLayout;          fieldTemplate.nativeWidth = nw; @@ -432,4 +478,12 @@ export namespace Doc {          d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout);          if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined;      } +    export async function UseDetailLayout(d: Doc) { +        let miniLayout = await PromiseValue(d.miniLayout); +        let detailLayout = await PromiseValue(d.detailedLayout); +        if (miniLayout && d.layout === miniLayout && detailLayout) { +            d.layout = detailLayout; +            d.nativeWidth = d.nativeHeight = undefined; +        } +    }  }
\ No newline at end of file diff --git a/src/new_fields/FieldSymbols.ts b/src/new_fields/FieldSymbols.ts index a436dcf2b..b5b3aa588 100644 --- a/src/new_fields/FieldSymbols.ts +++ b/src/new_fields/FieldSymbols.ts @@ -7,4 +7,4 @@ export const Id = Symbol("Id");  export const OnUpdate = Symbol("OnUpdate");  export const Parent = Symbol("Parent");  export const Copy = Symbol("Copy"); -export const ToScriptString = Symbol("Copy");
\ No newline at end of file +export const ToScriptString = Symbol("ToScriptString");
\ No newline at end of file diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index 39c6c8ce3..8f64c1c2e 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -19,6 +19,8 @@ export interface StrokeData {      page: number;  } +export type InkData = Map<string, StrokeData>; +  const pointSchema = createSimpleSchema({      x: true, y: true  }); @@ -31,9 +33,9 @@ const strokeDataSchema = createSimpleSchema({  @Deserializable("ink")  export class InkField extends ObjectField {      @serializable(map(object(strokeDataSchema))) -    readonly inkData: Map<string, StrokeData>; +    readonly inkData: InkData; -    constructor(data?: Map<string, StrokeData>) { +    constructor(data?: InkData) {          super();          this.inkData = data || new Map;      } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index a2133a990..0c7b77fa5 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -1,4 +1,4 @@ -import { Deserializable, autoObject } from "../client/util/SerializationHelper"; +import { Deserializable, autoObject, afterDocDeserialize } from "../client/util/SerializationHelper";  import { Field } from "./Doc";  import { setter, getter, deleteProperty, updateFunction } from "./util";  import { serializable, alias, list } from "serializr"; @@ -254,7 +254,7 @@ class ListImpl<T extends Field> extends ObjectField {      [key: number]: T | (T extends RefField ? Promise<T> : never); -    @serializable(alias("fields", list(autoObject()))) +    @serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize })))      private get __fields() {          return this.___fields;      } diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index 38d874a68..14f08814e 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -48,7 +48,7 @@ export class ProxyField<T extends RefField> extends ObjectField {      private failed = false;      private promise?: Promise<any>; -    value(): T | undefined | FieldWaiting { +    value(): T | undefined | FieldWaiting<T> {          if (this.cache) {              return this.cache;          } @@ -63,6 +63,6 @@ export class ProxyField<T extends RefField> extends ObjectField {                  return field;              }));          } -        return this.promise; +        return this.promise as any;      }  } diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts index 75ce4287f..f7bea8c94 100644 --- a/src/new_fields/RefField.ts +++ b/src/new_fields/RefField.ts @@ -1,10 +1,11 @@  import { serializable, primitive, alias } from "serializr";  import { Utils } from "../Utils";  import { Id, HandleUpdate, ToScriptString } from "./FieldSymbols"; +import { afterDocDeserialize } from "../client/util/SerializationHelper";  export type FieldId = string;  export abstract class RefField { -    @serializable(alias("id", primitive())) +    @serializable(alias("id", primitive({ afterDeserialize: afterDocDeserialize })))      private __id: FieldId;      readonly [Id]: FieldId; @@ -13,7 +14,7 @@ export abstract class RefField {          this[Id] = this.__id;      } -    protected [HandleUpdate]?(diff: any): void; +    protected [HandleUpdate]?(diff: any): void | Promise<void>;      abstract [ToScriptString](): string;  } diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts index e5ec34f57..00b4dec2c 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -2,10 +2,11 @@ import { ObjectField } from "./ObjectField";  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"; +import { Deserializable, autoObject } from "../client/util/SerializationHelper";  import { Doc } from "../new_fields/Doc";  import { Plugins } from "./util";  import { computedFn } from "mobx-utils"; +import { ProxyField } from "./Proxy";  function optional(propSchema: PropSchema) {      return custom(value => { @@ -34,7 +35,16 @@ const scriptSchema = createSimpleSchema({      originalScript: true  }); -function deserializeScript(script: ScriptField) { +async function deserializeScript(script: ScriptField) { +    const captures: ProxyField<Doc> = (script as any).captures; +    if (captures) { +        const doc = (await captures.value())!; +        const captured: any = {}; +        const keys = Object.keys(doc); +        const vals = await Promise.all(keys.map(key => doc[key]) as any); +        keys.forEach((key, i) => captured[key] = vals[i]); +        (script.script.options as any).capturedVariables = captured; +    }      const comp = CompileScript(script.script.originalScript, script.script.options);      if (!comp.compiled) {          throw new Error("Couldn't compile loaded script"); @@ -48,9 +58,17 @@ export class ScriptField extends ObjectField {      @serializable(object(scriptSchema))      readonly script: CompiledScript; +    @serializable(autoObject()) +    private captures?: ProxyField<Doc>; +      constructor(script: CompiledScript) {          super(); +        if (script && script.options.capturedVariables) { +            const doc = Doc.assign(new Doc, script.options.capturedVariables); +            this.captures = new ProxyField(doc); +        } +          this.script = script;      } diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index 700269727..48b8fe3fa 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -15,7 +15,9 @@ source = "./source"  dist = "../../server/public/files"  db = MongoClient("localhost", 27017)["Dash"] +target_collection = db.newDocuments  schema_guids = [] +common_proto_id = ""  def extract_links(fileName): @@ -84,7 +86,7 @@ def write_schema(parse_results, display_fields, storage_key):              "height": 600,              "panX": 0,              "panY": 0, -            "zoomBasis": 0.5, +            "zoomBasis": 1,              "zIndex": 2,              "libraryBrush": False,              "viewType": 2 @@ -92,7 +94,7 @@ def write_schema(parse_results, display_fields, storage_key):          "__type": "Doc"      } -    fields["proto"] = protofy("collectionProto") +    fields["proto"] = protofy(common_proto_id)      fields[storage_key] = listify(proxify_guids(view_guids))      fields["schemaColumns"] = listify(display_fields)      fields["backgroundColor"] = "white" @@ -106,8 +108,8 @@ def write_schema(parse_results, display_fields, storage_key):      fields["isPrototype"] = True      fields["page"] = -1 -    db.newDocuments.insert_one(data_doc) -    db.newDocuments.insert_one(view_doc) +    target_collection.insert_one(data_doc) +    target_collection.insert_one(view_doc)      data_doc_guid = data_doc["_id"]      print(f"inserted view document ({view_doc_guid})") @@ -136,7 +138,7 @@ def write_text_doc(content):      data_doc = {          "_id": data_doc_guid,          "fields": { -            "proto": protofy("textProto"), +            "proto": protofy("commonImportProto"),              "data": {                  "Data": '{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"' + content + '"}]}]},"selection":{"type":"text","anchor":1,"head":1}' + '}',                  "__type": "RichTextField" @@ -158,8 +160,8 @@ def write_text_doc(content):          "__type": "Doc"      } -    db.newDocuments.insert_one(view_doc) -    db.newDocuments.insert_one(data_doc) +    target_collection.insert_one(view_doc) +    target_collection.insert_one(data_doc)      return view_doc_guid @@ -209,8 +211,8 @@ def write_image(folder, name):          "__type": "Doc"      } -    db.newDocuments.insert_one(view_doc) -    db.newDocuments.insert_one(data_doc) +    target_collection.insert_one(view_doc) +    target_collection.insert_one(data_doc)      return view_doc_guid @@ -347,6 +349,22 @@ def proxify_guids(guids):      return list(map(lambda guid: {"fieldId": guid, "__type": "proxy"}, guids)) +def write_common_proto(): +    id = guid() +    common_proto = { +        "_id": id, +        "fields": { +            "proto": protofy("collectionProto"), +            "title": "Common Import Proto", +        }, +        "__type": "Doc" +    } + +    target_collection.insert_one(common_proto) + +    return id + +  if os.path.exists(dist):      shutil.rmtree(dist)  while os.path.exists(dist): @@ -354,6 +372,8 @@ while os.path.exists(dist):  os.mkdir(dist)  mkdir_if_absent(source) +common_proto_id = write_common_proto() +  candidates = 0  for file_name in os.listdir(source):      if file_name.endswith('.docx'): @@ -372,7 +392,7 @@ parent_guid = write_schema({  }, ["title", "short_description", "original_price"], "data")  print("appending parent schema to main workspace...\n") -db.newDocuments.update_one( +target_collection.update_one(      {"fields.title": "WS collection 1"},      {"$push": {"fields.data.fields": {"fieldId": parent_guid, "__type": "proxy"}}}  ) diff --git a/src/server/index.ts b/src/server/index.ts index 5b086a2cf..40c0e7981 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -139,6 +139,16 @@ app.get("/pull", (req, res) =>          res.redirect("/");      })); +app.get("/version", (req, res) => { +    exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => { +        if (err) { +            res.send(err.message); +            return; +        } +        res.send(stdout); +    }); +}); +  // SEARCH  // GETTERS @@ -284,18 +294,15 @@ addSecureRoute(      RouteStore.getCurrUser  ); +const ServicesApiKeyMap = new Map<string, string | undefined>([ +    ["face", process.env.FACE], +    ["vision", process.env.VISION], +    ["handwriting", process.env.HANDWRITING] +]); +  addSecureRoute(Method.GET, (user, res, req) => { -    let requested = req.params.requestedservice; -    switch (requested) { -        case "face": -            res.send(process.env.FACE); -            break; -        case "vision": -            res.send(process.env.VISION); -            break; -        default: -            res.send(undefined); -    } +    let service = req.params.requestedservice; +    res.send(ServicesApiKeyMap.get(service));  }, undefined, `${RouteStore.cognitiveServices}/:requestedservice`);  class NodeCanvasFactory {  | 
