From b2aa0b80843e6e58f737e2937c45351d653255fa Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 04:18:01 -0500
Subject: pivot field box and proper AND/OR behavior of facets

---
 src/client/views/collections/CollectionSubView.tsx | 29 ++++++++++++++++------
 1 file changed, 22 insertions(+), 7 deletions(-)

(limited to 'src/client/views/collections/CollectionSubView.tsx')

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index db3fde797..b96b86929 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -109,15 +109,30 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
             const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
+            const clusters: { [key: string]: { [value: string]: string } } = {};
+            for (let i = 0; i < docFilters.length; i += 3) {
+                const [key, value, modifiers] = docFilters.slice(i, i + 3);
+                const cluster = clusters[key];
+                if (!cluster) {
+                    const child: { [value: string]: string } = {};
+                    child[value] = modifiers;
+                    clusters[key] = child;
+                } else {
+                    cluster[value] = modifiers;
+                }
+            }
             const filteredDocs = docFilters.length ? viewedDocs.filter(d => {
-                let result = false;
-                for (let i = 0; i < docFilters.length; i += 3) {
-                    const key = docFilters[i];
-                    const value = docFilters[i + 1];
-                    const modifiers = docFilters[i + 2];
-                    result = result || ((modifiers === "x") !== Doc.matchFieldValue(d, key, value));
+                for (const key of Object.keys(clusters)) {
+                    const cluster = clusters[key];
+                    const satisfiesFacet = Object.keys(cluster).some(inner => {
+                        const modifier = cluster[inner];
+                        return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
+                    });
+                    if (!satisfiesFacet) {
+                        return false;
+                    }
                 }
-                return result;
+                return true;
             }) : viewedDocs;
             return filteredDocs;
         }
-- 
cgit v1.2.3-70-g09d2


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

---
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 ++--
 src/client/util/Import & Export/ImageUtils.ts      |  4 ++--
 src/client/views/DocComponent.tsx                  |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/nodes/AudioBox.tsx                |  2 +-
 src/client/views/nodes/ImageBox.scss               | 24 ++++++++++++++-----
 src/client/views/nodes/ImageBox.tsx                | 28 +++++++++++++++++++++-
 src/mobile/ImageUpload.tsx                         |  2 +-
 src/server/ApiManagers/UploadManager.ts            | 14 ++++++++++-
 src/server/DashUploadUtils.ts                      | 12 +++-------
 10 files changed, 69 insertions(+), 25 deletions(-)

(limited to 'src/client/views/collections/CollectionSubView.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c7c94abed..071015193 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -116,13 +116,13 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
+            collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData)));
             runInAction(() => this.completed += batch.length);
         });
 
         await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
             const path = Utils.prepend(clientAccessPath);
-            const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
+            const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 2f4db0e17..ff909cc6b 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -22,8 +22,8 @@ export namespace ImageUtils {
         } = await Networking.PostToServer("/inspectImage", { source });
         document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
         const proto = Doc.GetProto(document);
-        proto.nativeWidth = nativeWidth;
-        proto.nativeHeight = nativeHeight;
+        proto["data-nativeWidth"] = nativeWidth;
+        proto["data-nativeHeight"] = nativeHeight;
         proto.contentSize = contentSize;
         return data !== undefined;
     };
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d98301cf6..6f3eb6808 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -61,7 +61,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
 
         _annotationKey: string = "annotations";
         public set annotationKey(val: string) { this._annotationKey = val; }
-        public get annotationKey() { return this._annotationKey }
+        public get annotationKey() { return this._annotationKey; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b96b86929..80fcc33aa 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -324,7 +324,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
 
                     formData.append('file', file);
                     const dropFileName = file ? file.name : "-empty-";
-                    promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
+                    promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
                         results.map(action((result: any) => {
                             const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result;
                             const full = { ...options, _width: 300, title: dropFileName };
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8ede79edc..62a479b2a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -138,7 +138,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             self._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
                 });
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index cf5d999a7..43f4a0ba9 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,10 +1,12 @@
-.imageBox, .imageBox-dragging{
+.imageBox,
+.imageBox-dragging {
     pointer-events: all;
     border-radius: inherit;
-    width:100%;
-    height:100%;
+    width: 100%;
+    height: 100%;
     position: absolute;
     transform-origin: top left;
+
     .imageBox-fader {
         pointer-events: all;
     }
@@ -16,6 +18,14 @@
     }
 }
 
+#upload-icon {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    width: 20px;
+    height: 20px;
+}
+
 .imageBox-cont {
     padding: 0vw;
     position: absolute;
@@ -25,7 +35,8 @@
     max-width: 100%;
     max-height: 100%;
     pointer-events: none;
-    background:transparent;
+    background: transparent;
+
     img {
         height: auto;
         width: 100%;
@@ -55,9 +66,10 @@
     padding: 3px;
     background: white;
     cursor: pointer;
-    opacity:0.15;
+    opacity: 0.15;
 }
-#google-photos:hover{
+
+#google-photos:hover {
     opacity: 1;
 }
 
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>;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index dab4de110..1583e3d5d 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -45,7 +45,7 @@ class Uploader extends React.Component {
                     const formData = new FormData();
                     formData.append("file", files[0]);
 
-                    const upload = window.location.origin + "/upload";
+                    const upload = window.location.origin + "/uploadFormData";
                     this.status = "uploading image";
                     const res = await fetch(upload, {
                         method: 'POST',
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 583eaa59b..a92b613b7 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -40,7 +40,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: "/upload",
+            subscription: "/uploadFormData",
             secureHandler: async ({ req, res }) => {
                 const form = new formidable.IncomingForm();
                 form.uploadDir = pathToDirectory(Directory.parsed_files);
@@ -59,6 +59,18 @@ export default class UploadManager extends ApiManager {
             }
         });
 
+        register({
+            method: Method.POST,
+            subscription: "/uploadRemoteImage",
+            secureHandler: async ({ req, res }) => {
+                const { sources } = req.body;
+                if (Array.isArray(sources)) {
+                    return res.send(await Promise.all(sources.map(url => DashUploadUtils.UploadImage(url))));
+                }
+                res.send();
+            }
+        });
+
         register({
             method: Method.POST,
             subscription: "/uploadDoc",
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index b826b4882..cb7104757 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -108,13 +108,7 @@ export namespace DashUploadUtils {
         return MoveParsedFile(absolutePath, Directory.pdfs);
     }
 
-    const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
-    const sanitizeExtension = (source: string) => {
-        let extension = path.extname(source);
-        extension = extension.toLowerCase();
-        extension = extension.split("?")[0];
-        return extension;
-    };
+    const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
 
     /**
      * Uploads an image specified by the @param source to Dash's /public/files/
@@ -209,8 +203,8 @@ export namespace DashUploadUtils {
 
     export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
         const { requestable, source, ...remaining } = metadata;
-        const resolved = filename || generate(prefix, requestable);
-        const extension = format || sanitizeExtension(requestable || resolved);
+        const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
+        const resolved = filename || generate(prefix, extension);
         const information: ImageUploadInformation = {
             clientAccessPath: clientPathToFile(Directory.images, resolved),
             serverAccessPaths: {},
-- 
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/collections/CollectionSubView.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

