From c5d618538d4fd9669476dc7eb66ddd45783e5fa6 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 05:50:15 -0500
Subject: nativewidth and nativeheight extension key fix and implemented UI to
 manually download a remotely hosted image

---
 src/client/views/nodes/ImageBox.tsx | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8db66d367..3d15bcb15 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -29,6 +29,7 @@ import { TraceMobx } from '../../../new_fields/util';
 import { SelectionManager } from '../../util/SelectionManager';
 import { cache } from 'sharp';
 import { ObjectField } from '../../../new_fields/ObjectField';
+import { Networking } from '../../Network';
 const requestImageSize = require('../../util/request-image-size');
 const path = require('path');
 const { Howl } = require('howler');
@@ -104,7 +105,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend("/upload"), {
+                const res = await fetch(Utils.prepend("/uploadFormData"), {
                     method: 'POST',
                     body: formData
                 });
@@ -287,6 +288,30 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
     }
 
+    @computed
+    private get considerDownloadIcon() {
+        const data = this.dataDoc[this.props.fieldKey];
+        if (!(data instanceof ImageField)) {
+            return (null);
+        }
+        const primary = data.url.href;
+        if (primary.includes(window.location.origin)) {
+            return (null);
+        }
+        return (
+            <img
+                id={"upload-icon"}
+                src={"/assets/downarrow.png"}
+                onClick={async () => {
+                    const { dataDoc } = this;
+                    const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
+                    dataDoc.originalUrl = primary;
+                    dataDoc[this.props.fieldKey] = new ImageField(Utils.prepend(clientAccessPath));
+                }}
+            />
+        );
+    }
+
     @computed get nativeSize() {
         const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
         const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw);
@@ -347,6 +372,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
                     icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
             </div>
+            {this.considerDownloadIcon}
             {this.considerGooglePhotosLink()}
             <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
         </div>;
-- 
cgit v1.2.3-70-g09d2


From bb768ca2a9274d07ef4618e527452dcdd6cd430d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 13:19:50 -0500
Subject: fallback behavior for non-downloaded images and restored ink
 recognition

---
 src/Utils.ts                                       |  2 +-
 src/client/cognitive_services/CognitiveServices.ts |  3 +-
 src/client/views/InkingStroke.tsx                  | 34 ++++++++++++++++++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 19 +-----------
 src/client/views/nodes/ImageBox.tsx                |  8 +++--
 5 files changed, 38 insertions(+), 28 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 7bf05a6fc..4deac9035 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -48,7 +48,7 @@ export namespace Utils {
     }
 
     export async function getApiKey(target: string): Promise<string> {
-        const response = await fetch(prepend(`environment/${target.toUpperCase()}`));
+        const response = await fetch(prepend(`/environment/${target.toUpperCase()}`));
         return response.text();
     }
 
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 57296c961..9e2ceac62 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -185,8 +185,9 @@ export namespace CognitiveServices {
                     results.recognitionUnits && (results = results.recognitionUnits);
                     target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis");
                     const recognizedText = results.map((item: any) => item.recognizedText);
+                    const recognizedObjects = results.map((item: any) => item.recognizedObject);
                     const individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
-                    target[keys[1]] = individualWords.join(" ");
+                    target[keys[1]] = individualWords.length ? individualWords.join(" ") : recognizedObjects.join(", ");
                 }
 
                 batch.end();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index aca507147..f315ce12a 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -11,6 +11,12 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
 import { TraceMobx } from "../../new_fields/util";
 import { InteractionUtils } from "../util/InteractionUtils";
+import { ContextMenu } from "./ContextMenu";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
+import { library } from "@fortawesome/fontawesome-svg-core";
+
+library.add(faPaintBrush);
 
 type InkDocument = makeInterface<[typeof documentSchema]>;
 const InkDocument = makeInterface(documentSchema);
@@ -22,6 +28,11 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
     @computed get PanelWidth() { return this.props.PanelWidth(); }
     @computed get PanelHeight() { return this.props.PanelHeight(); }
 
+    private analyzeStrokes = () => {
+        const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.Document, ["inkAnalysis", "handwriting"], [data]);
+    }
+
     render() {
         TraceMobx();
         const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
@@ -37,12 +48,23 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
         const scaleX = this.PanelWidth / width;
         const scaleY = this.PanelHeight / height;
         return (
-            <svg width={width} height={height} style={{
-                transformOrigin: "top left",
-                transform: `scale(${scaleX}, ${scaleY})`,
-                mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset",
-                pointerEvents: "all"
-            }}>
+            <svg
+                width={width}
+                height={height}
+                style={{
+                    transformOrigin: "top left",
+                    transform: `scale(${scaleX}, ${scaleY})`,
+                    mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset",
+                    pointerEvents: "all"
+                }}
+                onContextMenu={() => {
+                    ContextMenu.Instance.addItem({
+                        description: "Analyze Stroke",
+                        event: this.analyzeStrokes,
+                        icon: "paint-brush"
+                    });
+                }}
+            >
                 {points}
             </svg>
         );
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 70f8a0e73..4ba58ac84 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -819,23 +819,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }, "arrange contents");
     }
 
-
-    analyzeStrokes = async () => {
-        const children = await DocListCastAsync(this.dataDoc.data);
-        if (!children) {
-            return;
-        }
-        const inkData: InkData[] = [];
-        for (const doc of children) {
-            const data = Cast(doc.data, InkField)?.inkData;
-            data && inkData.push(data);
-        }
-        if (!inkData.length) {
-            return;
-        }
-        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData);
-    }
-
     private thumbIdentifier?: number;
 
     // @action
@@ -887,7 +870,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
         layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
         layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
-        layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+        // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
         layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
         layoutItems.push({
             description: "Import document", icon: "upload", event: ({ x, y }) => {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 3d15bcb15..adec13e32 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fie
 import { List } from '../../../new_fields/List';
 import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
 import { ComputedField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast } from '../../../new_fields/Types';
+import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
 import { AudioField, ImageField } from '../../../new_fields/URLField';
 import { Utils, returnOne, emptyFunction } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
@@ -214,11 +214,15 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         if (this._curSuffix === "_m") this._mediumRetryCount++;
         if (this._curSuffix === "_l") this._largeRetryCount++;
     }
-    @action onError = () => {
+    @action onError = (error: any) => {
         const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
         if (timeout < 10) {
             // setTimeout(this.retryPath, 500);
         }
+        const original = StrCast(this.dataDoc.originalUrl);
+        if (error.type === "error" && original) {
+            this.dataDoc[this.props.fieldKey] = new ImageField(original);
+        }
     }
     _curSuffix = "_m";
 
-- 
cgit v1.2.3-70-g09d2


From b049149fcf852d8a83338a24341083653fecff89 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 16:59:36 -0500
Subject: added UI responsiveness for remote image upload routine

---
 deploy/assets/greencheck.png                       | Bin 0 -> 49332 bytes
 deploy/assets/redx.png                             | Bin 0 -> 7353 bytes
 src/client/documents/Documents.ts                  |   2 +-
 src/client/views/collections/CollectionSubView.tsx |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  21 +++++++++++++++++++--
 5 files changed, 21 insertions(+), 4 deletions(-)
 create mode 100644 deploy/assets/greencheck.png
 create mode 100644 deploy/assets/redx.png

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/deploy/assets/greencheck.png b/deploy/assets/greencheck.png
new file mode 100644
index 000000000..064e9def1
Binary files /dev/null and b/deploy/assets/greencheck.png differ
diff --git a/deploy/assets/redx.png b/deploy/assets/redx.png
new file mode 100644
index 000000000..0c2c9ccc5
Binary files /dev/null and b/deploy/assets/redx.png differ
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ac643f91f..a7f939d7d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -324,7 +324,7 @@ export namespace Docs {
             // whatever options pertain to this specific prototype
             const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
             options.layout = layout.view.LayoutString(layout.dataField);
-            let doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options });
+            const doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options });
             return doc;
         }
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 80fcc33aa..321a6d34c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -70,7 +70,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 (args) => {
                     const childLayout = Cast(this.props.Document.childLayout, Doc);
                     if (childLayout instanceof Doc) {
-                        this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout as Doc, doc, "layoutFromParent"));
+                        this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout, doc, "layoutFromParent"));
                     }
                     else if (!(childLayout instanceof Promise)) {
                         this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout"));
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index adec13e32..3f25faae6 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -58,6 +58,8 @@ declare class MediaRecorder {
 type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
 const ImageDocument = makeInterface(pageSchema, documentSchema);
 
+const defaultUploadIcon = "downarrow.png";
+
 @observer
 export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
@@ -65,6 +67,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _audioState = 0;
     @observable static _showControls: boolean;
+    @observable uploadIcon = defaultUploadIcon;
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer && this._dropDisposer();
@@ -305,12 +308,26 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         return (
             <img
                 id={"upload-icon"}
-                src={"/assets/downarrow.png"}
+                src={`/assets/${this.uploadIcon}`}
                 onClick={async () => {
                     const { dataDoc } = this;
+                    runInAction(() => this.uploadIcon = "loading.gif");
                     const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
                     dataDoc.originalUrl = primary;
-                    dataDoc[this.props.fieldKey] = new ImageField(Utils.prepend(clientAccessPath));
+                    let success = true;
+                    let data: ImageField | undefined;
+                    try {
+                        data = new ImageField(clientAccessPath);
+                    } catch {
+                        success = false;
+                    }
+                    runInAction(() => this.uploadIcon = success ? "greencheck.png" : "redx.png");
+                    setTimeout(action(() => {
+                        this.uploadIcon = defaultUploadIcon;
+                        if (data) {
+                            dataDoc[this.props.fieldKey] = data;
+                        }
+                    }), 2000);
                 }}
             />
         );
-- 
cgit v1.2.3-70-g09d2


From 9a9524b4c6e2980eb37ece288890d6cb746eb81c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 17:10:34 -0500
Subject: factored out icon names

---
 src/client/views/nodes/ImageBox.tsx | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 3f25faae6..ffcbe459e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -58,7 +58,12 @@ declare class MediaRecorder {
 type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
 const ImageDocument = makeInterface(pageSchema, documentSchema);
 
-const defaultUploadIcon = "downarrow.png";
+const uploadIcons = {
+    idle: "downarrow.png",
+    loading: "loading.gif",
+    success: "greencheck.png",
+    failure: "redx.png"
+};
 
 @observer
 export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
@@ -67,7 +72,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _audioState = 0;
     @observable static _showControls: boolean;
-    @observable uploadIcon = defaultUploadIcon;
+    @observable uploadIcon = uploadIcons.idle;
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer && this._dropDisposer();
@@ -311,19 +316,20 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 src={`/assets/${this.uploadIcon}`}
                 onClick={async () => {
                     const { dataDoc } = this;
-                    runInAction(() => this.uploadIcon = "loading.gif");
+                    const { success, failure, idle, loading } = uploadIcons;
+                    runInAction(() => this.uploadIcon = loading);
                     const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
                     dataDoc.originalUrl = primary;
-                    let success = true;
+                    let succeeded = true;
                     let data: ImageField | undefined;
                     try {
-                        data = new ImageField(clientAccessPath);
+                        data = new ImageField(Utils.prepend(clientAccessPath));
                     } catch {
-                        success = false;
+                        succeeded = false;
                     }
-                    runInAction(() => this.uploadIcon = success ? "greencheck.png" : "redx.png");
+                    runInAction(() => this.uploadIcon = succeeded ? success : failure);
                     setTimeout(action(() => {
-                        this.uploadIcon = defaultUploadIcon;
+                        this.uploadIcon = idle;
                         if (data) {
                             dataDoc[this.props.fieldKey] = data;
                         }
-- 
cgit v1.2.3-70-g09d2

