From 6f8f9bd2eaba36dc64e7335dee53a372542bad46 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Mon, 22 Jul 2019 17:17:09 -0400
Subject: basic stuff working, column headers don tmatch add group button

---
 .../CollectionStackingViewFieldColumn.tsx          | 202 +++++++++++++++++++++
 1 file changed, 202 insertions(+)
 create mode 100644 src/client/views/collections/CollectionStackingViewFieldColumn.tsx

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

diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
new file mode 100644
index 000000000..9f64a4e93
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -0,0 +1,202 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { number } from "prop-types";
+import { Doc, WidthSym } from "../../../new_fields/Doc";
+import { CollectionStackingView } from "./CollectionStackingView";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Utils } from "../../../Utils";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { EditableView } from "../EditableView";
+import { action, observable } from "mobx";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import "./CollectionStackingView.scss";
+import { Docs } from "../../documents/Documents";
+
+
+interface CSVFieldColumnProps {
+    cols: () => number;
+    headings: () => object[];
+    heading: string;
+    docList: Doc[];
+    parent: CollectionStackingView;
+    type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+    createDropTarget: (ele: HTMLDivElement) => void;
+}
+
+@observer
+export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> {
+    @observable private _background = "white";
+
+    private _dropRef: HTMLDivElement | null = null;
+    private dropDisposer?: DragManager.DragDropDisposer;
+
+    createColumnDropRef = (ele: HTMLDivElement | null) => {
+        this._dropRef = ele;
+        this.dropDisposer && this.dropDisposer();
+        if (ele) {
+            this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.columnDrop.bind(this) } });
+        }
+    }
+
+    @undoBatch
+    @action
+    columnDrop = (e: Event, de: DragManager.DropEvent) => {
+        if (de.data instanceof DragManager.DocumentDragData) {
+            let key = StrCast(this.props.parent.props.Document.sectionFilter);
+            let castedValue = this.getValue(this.props.heading);
+            if (castedValue) {
+                de.data.droppedDocuments.forEach(d => d[key] = castedValue);
+            }
+            this.props.parent.drop(e, de);
+            e.stopPropagation();
+        }
+    }
+
+    children(docs: Doc[]) {
+        let style = this.props.parent;
+        this.props.parent._docXfs.length = 0;
+        return docs.map((d, i) => {
+            let layoutDoc = Doc.expandTemplateLayout(d, this.props.parent.props.DataDoc);
+            let headings = this.props.headings();
+            let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+            let width = () => (d.nativeWidth ? Math.min(layoutDoc[WidthSym](), style.columnWidth) : style.columnWidth) / (uniqueHeadings.length + 1);
+            let height = () => this.props.parent.getDocHeight(layoutDoc);
+            if (style.singleColumn) {
+                let dxf;
+                let dref = React.createRef<HTMLDivElement>();
+                if (uniqueHeadings.length > 0) {
+                    dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+                    this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+                }
+                else {
+                    //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
+                    dxf = () => this.getSingleDocTransform(layoutDoc, i, width());
+                    this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+                }
+                let rowHgtPcnt = height();
+                return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} style={{ width: width(), marginTop: i === 0 ? 0 : style.gridGap, height: `${rowHgtPcnt}` }} >
+                    {this.props.parent.getDisplayDoc(layoutDoc, d, dxf)}
+                </div>;
+            } else {
+                let dref = React.createRef<HTMLDivElement>();
+                let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+                let rowSpan = Math.ceil((height() + style.gridGap) / style.gridGap);
+                this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+                return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} >
+                    {this.props.parent.getDisplayDoc(layoutDoc, d, dxf)}
+                </div>;
+            }
+        });
+    }
+
+    getSingleDocTransform(doc: Doc, ind: number, width: number) {
+        let localY = this.props.parent.filteredChildren.reduce((height, d, i) =>
+            height + (i < ind ? this.props.parent.getDocHeight(Doc.expandTemplateLayout(d, this.props.parent.props.DataDoc)) + this.props.parent.gridGap : 0), this.props.parent.yMargin);
+        let translate = this.props.parent.props.ScreenToLocalTransform().inverse().transformPoint((this.props.parent.props.PanelWidth() - width) / 2, localY);
+        return this.offsetTransform(doc, translate[0], translate[1]);
+    }
+
+    offsetTransform(doc: Doc, translateX: number, translateY: number) {
+        let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
+        let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+        return this.props.parent.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
+    }
+
+    getDocTransform(doc: Doc, dref: HTMLDivElement) {
+        let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+        return this.offsetTransform(doc, translateX, translateY);
+    }
+
+    getValue = (value: string): any => {
+        let parsed = parseInt(value);
+        if (!isNaN(parsed)) {
+            return parsed;
+        }
+        if (value.toLowerCase().indexOf("true") > -1) {
+            return true;
+        }
+        if (value.toLowerCase().indexOf("false") > -1) {
+            return false;
+        }
+        return value;
+    }
+
+    headingChanged = (value: string, shiftDown?: boolean) => {
+        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        let castedValue = this.getValue(value);
+        if (castedValue) {
+            this.props.docList.forEach(d => d[key] = castedValue);
+            return true;
+        }
+        return false;
+    }
+
+    @action
+    pointerEntered = () => {
+        if (SelectionManager.GetIsDragging()) {
+            this._background = "#b4b4b4";
+        }
+    }
+
+    @action
+    pointerLeave = () => {
+        this._background = "white";
+    }
+
+    @action
+    addDocument = () => {
+        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        let newDoc = Docs.Create.TextDocument({ height: 18, title: "new text document" });
+        newDoc[key] = this.getValue(this.props.heading);
+        this.props.parent.props.addDocument(newDoc);
+    }
+
+    render() {
+        let cols = this.props.cols();
+        let templatecols = "";
+        let headings = this.props.headings();
+        let heading = this.props.heading;
+        let style = this.props.parent;
+        let singleColumn = style.singleColumn;
+        let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+        let editableViewProps = {
+            GetValue: () => heading,
+            SetValue: this.headingChanged,
+            contents: heading,
+        }
+        let headingView = heading ?
+            <div key={heading} className="collectionStackingView-sectionHeader"
+                style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}>
+                <EditableView {...editableViewProps} />
+            </div> : (null);
+        for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `;
+        return (
+            <div key={heading} style={{ width: `${100 / (uniqueHeadings.length + 1)}%`, background: this._background }}
+                ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
+                {headingView}
+                <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
+                    style={{
+                        padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
+                        margin: "auto",
+                        width: singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+                        height: 'max-content',
+                        position: "relative",
+                        gridGap: style.gridGap,
+                        gridTemplateColumns: singleColumn ? undefined : templatecols,
+                        gridAutoRows: singleColumn ? undefined : "0px"
+                    }}
+                >
+                    {this.children(this.props.docList)}
+                    {singleColumn ? (null) : this.props.parent.columnDragger}
+                </div>
+                <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+                    style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}>
+                    <button style={{ width: "100%" }} onClick={this.addDocument}>+ New</button>
+                </div>
+            </div>
+        );
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From de9b03ca6c7116949952bc428e8d9257959ad974 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Tue, 23 Jul 2019 13:58:45 -0400
Subject: implemented most of the board functionality

---
 .../views/collections/CollectionStackingView.scss  |  99 +++++++++++++++---
 .../views/collections/CollectionStackingView.tsx   | 115 ++++++++++++++-------
 .../CollectionStackingViewFieldColumn.tsx          |  64 ++++++++++--
 src/new_fields/SchemaHeaderField.ts                |  46 +++++++++
 4 files changed, 266 insertions(+), 58 deletions(-)
 create mode 100644 src/new_fields/SchemaHeaderField.ts

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

diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 3e389225a..e0ced8af4 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -6,6 +6,7 @@
     position: absolute;
     display: flex;
     overflow-y: auto;
+    flex-wrap: wrap;
 
     .collectionStackingView-docView-container {
         width: 45%;
@@ -67,12 +68,6 @@
         display: inline-block;
     }
 
-    .collectionStackingView-columnDoc,
-    .collectionStackingView-masonryDoc {
-        margin-left: auto;
-        margin-right: auto;
-    }
-
     .collectionStackingView-masonryDoc {
         transform-origin: top left;
         grid-column-end: span 1;
@@ -80,27 +75,103 @@
     }
 
     .collectionStackingView-sectionHeader {
-        background: gray;
         text-align: center;
-        margin-left: 10px;
-        margin-right: 10px;
+        margin-left: 5px;
+        margin-right: 5px;
         margin-top: 10px;
-        color: $light-color;
-        text-transform: uppercase;
-        letter-spacing: 2px;
-        padding: 10px;
+        overflow: hidden;
 
         .editableView-input {
             color: black;
         }
+
+        .editableView-input:hover,
+        .editableView-container-editing:hover,
+        .editableView-container-editing-oneLine:hover {
+            cursor: text
+        }
+
+        .collectionStackingView-sectionHeader-subCont {
+            outline: none;
+            border: 0px;
+            color: $light-color;
+            text-transform: uppercase;
+            letter-spacing: 2px;
+            font-size: 75%;
+            transition: transform 0.2s;
+            position: relative;
+
+            .editableView-container-editing-oneLine,
+            .editableView-container-editing {
+                color: grey;
+                padding: 10px;
+            }
+
+            .editableView-input:hover,
+            .editableView-container-editing:hover,
+            .editableView-container-editing-oneLine:hover {
+                cursor: text
+            }
+
+            .editableView-input {
+                padding: 12px 10px 11px 10px;
+                border: 0px;
+                color: grey;
+                // font-size: 75%;
+                text-align: center;
+                text-transform: uppercase;
+                letter-spacing: 2px;
+                outline-color: black;
+            }
+        }
+
+        .collectionStackingView-sectionDelete {
+            position: absolute;
+            right: 0;
+            top: 0;
+            height: 100%;
+        }
     }
 
     .collectionStackingView-addDocumentButton,
     .collectionStackingView-addGroupButton {
         display: inline-block;
-        margin: 0 10px;
+        margin: 0 5px;
         overflow: hidden;
         width: 90%;
         color: lightgrey;
+        overflow: ellipses;
+    }
+
+    .collectionStackingView-addGroupButton {
+        background: rgb(238, 238, 238);
+        font-size: 75%;
+        text-align: center;
+        text-transform: uppercase;
+        letter-spacing: 2px;
+        height: fit-content;
+
+        .editableView-container-editing-oneLine,
+        .editableView-container-editing {
+            color: grey;
+            padding: 10px;
+        }
+
+        .editableView-input:hover,
+        .editableView-container-editing:hover,
+        .editableView-container-editing-oneLine:hover {
+            cursor: text
+        }
+
+        .editableView-input {
+            padding: 12px 10px 11px 10px;
+            border: 0px;
+            color: grey;
+            // font-size: 75%;
+            text-align: center;
+            text-transform: uppercase;
+            letter-spacing: 2px;
+            outline-color: black;
+        }
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 0ddd5528b..e4e6f92e3 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -2,9 +2,9 @@ import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc, HeightSym, WidthSym, DocListCast } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, NumCast, Cast, StrCast, FieldValue } from "../../../new_fields/Types";
 import { emptyFunction, Utils } from "../../../Utils";
 import { CollectionSchemaPreview } from "./CollectionSchemaView";
 import "./CollectionStackingView.scss";
@@ -15,46 +15,80 @@ import { DocumentType } from "../../documents/Documents";
 import { Transform } from "../../util/Transform";
 import { CursorProperty } from "csstype";
 import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
+import { listSpec } from "../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { List } from "../../../new_fields/List";
+import { EditableView } from "../EditableView";
+
+let valuesCreated = 1;
 
 @observer
 export class CollectionStackingView extends CollectionSubView(doc => doc) {
     _masonryGridRef: HTMLDivElement | null = null;
     _draggerRef = React.createRef<HTMLDivElement>();
     _heightDisposer?: IReactionDisposer;
+    _sectionFilterDisposer?: IReactionDisposer;
     _docXfs: any[] = [];
     _columnStart: number = 0;
     @observable private cursor: CursorProperty = "grab";
+    get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
     @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 filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
-
-    @computed get Sections() {
+    get Sections() {
         let sectionFilter = StrCast(this.props.Document.sectionFilter);
-        let fields = new Map<object, Doc[]>();
-        sectionFilter && this.filteredChildren.map(d => {
-            let sectionValue = (d[sectionFilter] ? d[sectionFilter] : "-undefined-") as object;
-            let parsed = parseInt(sectionValue.toString());
-            let castedSectionValue: any = sectionValue;
-            if (!isNaN(parsed)) {
-                castedSectionValue = parsed;
-            }
-            if (!fields.has(castedSectionValue)) fields.set(castedSectionValue, [d]);
-            else fields.get(castedSectionValue)!.push(d);
-        });
+        let sectionHeaders = this.sectionHeaders;
+        if (!sectionHeaders) {
+            this.props.Document.sectionHeaders = sectionHeaders = new List();
+        }
+        let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []]));
+        if (sectionFilter) {
+            this.filteredChildren.map(d => {
+                let sectionValue = (d[sectionFilter] ? d[sectionFilter] : `No ${sectionFilter} value`) as object;
+                // the next five lines ensures that floating point rounding errors don't create more than one section -syip
+                let parsed = parseInt(sectionValue.toString());
+                let castedSectionValue: any = sectionValue;
+                if (!isNaN(parsed)) {
+                    castedSectionValue = parsed;
+                }
+
+                // look for if header exists already
+                let existingHeader = sectionHeaders!.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `No ${sectionFilter} value`));
+                if (existingHeader) {
+                    fields.get(existingHeader)!.push(d);
+                }
+                else {
+                    let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `No ${sectionFilter} value`);
+                    fields.set(newSchemaHeader, [d]);
+                    sectionHeaders!.push(newSchemaHeader);
+                }
+            });
+        }
         return fields;
     }
     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 });
+        // is there any reason this needs to exist? -syip
+        // 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 });
+
+        // reset section headers when a new filter is inputted
+        this._sectionFilterDisposer = reaction(
+            () => StrCast(this.props.Document.sectionFilter),
+            () => {
+                this.props.Document.sectionHeaders = new List();
+                valuesCreated = 1;
+            }
+        )
     }
     componentWillUnmount() {
         this._heightDisposer && this._heightDisposer();
+        this._sectionFilterDisposer && this._sectionFilterDisposer();
     }
 
     @action
@@ -73,8 +107,8 @@ 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 headings = Array.from(this.Sections.keys());
-        let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
-        let width = () => (d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth) / (uniqueHeadings.length + 1);
+        // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+        let width = () => (d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth) / (headings.length + 1);
         let height = () => this.getDocHeight(layoutDoc);
         let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
         return <CollectionSchemaPreview
@@ -183,23 +217,21 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             }
         });
     }
-    section = (heading: string, docList: Doc[]) => {
+    section = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
         let key = StrCast(this.props.Document.sectionFilter);
-        let types = docList.map(d => typeof d[key]);
         let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
+        let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
         if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
             type = types[0];
         }
-        let parsed = parseInt(heading);
-        if (!isNaN(parsed)) {
-            heading = parsed.toString();
-        }
         let cols = () => this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
             Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
         return <CollectionStackingViewFieldColumn
+            key={heading ? heading.heading : ""}
             cols={cols}
             headings={() => Array.from(this.Sections.keys())}
-            heading={heading}
+            heading={heading ? heading.heading : ""}
+            headingObject={heading}
             docList={docList}
             parent={this}
             type={type}
@@ -207,13 +239,24 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     @action
-    addGroup = () => {
-
+    addGroup = (value: string) => {
+        if (value) {
+            if (this.sectionHeaders) {
+                this.sectionHeaders.push(new SchemaHeaderField(value));
+                return true;
+            }
+        }
+        return false;
     }
 
     render() {
         let headings = Array.from(this.Sections.keys());
-        let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+        let editableViewProps = {
+            GetValue: () => "",
+            SetValue: this.addGroup,
+            contents: "+ Add a Group"
+        }
+        // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
         return (
             <div className="collectionStackingView"
                 ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
@@ -222,12 +265,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                     ["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()).sort((a, b) => a[0].toString() > b[0].toString() ? 1 : -1).
-                    map(section => this.section(section[0].toString(), section[1] as Doc[])) :
-                    this.section("", this.filteredChildren)}
+                    map(section => this.section(section[0], section[1] as Doc[])) :
+                    this.section(undefined, this.filteredChildren)}
                 {this.props.Document.sectionFilter ?
                     <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
-                        style={{ width: this.columnWidth / (uniqueHeadings.length + 1), marginTop: 10 }}>
-                        <button style={{ width: "100%" }} onClick={this.addGroup}>+ Add a Group</button>
+                        style={{ width: (this.columnWidth / (headings.length + 1)) - 10, marginTop: 10 }}>
+                        <EditableView {...editableViewProps} />
                     </div> : null}
             </div>
         );
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 9f64a4e93..fe24c63c7 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -7,19 +7,22 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { Utils } from "../../../Utils";
 import { NumCast, StrCast } from "../../../new_fields/Types";
 import { EditableView } from "../EditableView";
-import { action, observable } from "mobx";
+import { action, observable, computed } from "mobx";
 import { undoBatch } from "../../util/UndoManager";
 import { DragManager } from "../../util/DragManager";
 import { DocumentManager } from "../../util/DocumentManager";
 import { SelectionManager } from "../../util/SelectionManager";
 import "./CollectionStackingView.scss";
 import { Docs } from "../../documents/Documents";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 
 interface CSVFieldColumnProps {
     cols: () => number;
     headings: () => object[];
     heading: string;
+    headingObject: SchemaHeaderField | undefined;
     docList: Doc[];
     parent: CollectionStackingView;
     type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
@@ -33,6 +36,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     private _dropRef: HTMLDivElement | null = null;
     private dropDisposer?: DragManager.DragDropDisposer;
 
+    @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this._dropRef = ele;
         this.dropDisposer && this.dropDisposer();
@@ -46,10 +51,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     columnDrop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
             let key = StrCast(this.props.parent.props.Document.sectionFilter);
-            let castedValue = this.getValue(this.props.heading);
+            let castedValue = this.getValue(this._heading);
             if (castedValue) {
                 de.data.droppedDocuments.forEach(d => d[key] = castedValue);
             }
+            else {
+                de.data.droppedDocuments.forEach(d => d[key] = undefined);
+            }
             this.props.parent.drop(e, de);
             e.stopPropagation();
         }
@@ -124,11 +132,21 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         return value;
     }
 
+    @action
     headingChanged = (value: string, shiftDown?: boolean) => {
         let key = StrCast(this.props.parent.props.Document.sectionFilter);
         let castedValue = this.getValue(value);
         if (castedValue) {
+            if (this.props.parent.sectionHeaders) {
+                if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
+                    return false;
+                }
+            }
             this.props.docList.forEach(d => d[key] = castedValue);
+            if (this.props.headingObject) {
+                this.props.headingObject.setHeading(castedValue.toString());
+                this._heading = this.props.headingObject.heading;
+            }
             return true;
         }
         return false;
@@ -154,23 +172,53 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         this.props.parent.props.addDocument(newDoc);
     }
 
+    @action
+    deleteColumn = () => {
+        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        this.props.docList.forEach(d => d[key] = undefined);
+        if (this.props.parent.sectionHeaders && this.props.headingObject) {
+            let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+            this.props.parent.sectionHeaders.splice(index, 1);
+        }
+    }
+
     render() {
         let cols = this.props.cols();
+        let key = StrCast(this.props.parent.props.Document.sectionFilter);
         let templatecols = "";
         let headings = this.props.headings();
-        let heading = this.props.heading;
+        let heading = this._heading;
         let style = this.props.parent;
         let singleColumn = style.singleColumn;
         let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+        let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `No ${key} value`;
         let editableViewProps = {
-            GetValue: () => heading,
+            GetValue: () => evContents,
             SetValue: this.headingChanged,
-            contents: heading,
+            contents: evContents,
+            oneLine: true
         }
-        let headingView = heading ?
+        let headingView = this.props.headingObject ?
             <div key={heading} className="collectionStackingView-sectionHeader"
                 style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}>
-                <EditableView {...editableViewProps} />
+                {/* the default bucket (no key value) has a tooltip that describes what it is.
+                    Further, it does not have a color and cannot be deleted. */}
+                <div className="collectionStackingView-sectionHeader-subCont"
+                    title={evContents === `No ${key} value` ?
+                        `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
+                    style={{
+                        width: "100%",
+                        background: this.props.headingObject && evContents !== `No ${key} value` ?
+                            this.props.headingObject.color : "lightgrey",
+                        color: "grey"
+                    }}>
+                    <EditableView {...editableViewProps} />
+                    {evContents === `No ${key} value` ?
+                        (null) :
+                        <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
+                            <FontAwesomeIcon icon="trash" />
+                        </button>}
+                </div>
             </div> : (null);
         for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `;
         return (
@@ -180,7 +228,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
                     style={{
                         padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
-                        margin: "auto",
+                        margin: "auto 5px",
                         width: singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
                         height: 'max-content',
                         position: "relative",
diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
new file mode 100644
index 000000000..284de3023
--- /dev/null
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -0,0 +1,46 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, createSimpleSchema, primitive } from "serializr";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString, OnUpdate } from "./FieldSymbols";
+import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+
+export const PastelSchemaPalette = new Map<string, string>([
+    ["purple", "#f5b5fc"],
+    ["green", "#96F7D2"],
+    ["yellow", "#F0F696"],
+    ["red", "#FCB1B1"]
+])
+
+@scriptingGlobal
+@Deserializable("schemaheader")
+export class SchemaHeaderField extends ObjectField {
+    @serializable(primitive())
+    heading: string;
+    color: string;
+
+    constructor(heading: string = "", color: string = Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * 4)]) {
+        super();
+
+        this.heading = heading;
+        this.color = color;
+    }
+
+    setHeading(heading: string) {
+        this.heading = heading;
+        this[OnUpdate]();
+    }
+
+    setColor(color: string) {
+        this.color = color;
+        this[OnUpdate]();
+    }
+
+    [Copy]() {
+        return new SchemaHeaderField(this.heading, this.color);
+    }
+
+    [ToScriptString]() {
+        return `invalid`;
+    }
+}
+
-- 
cgit v1.2.3-70-g09d2


From 828fdd80ab8af26f6e2ef521e05a71e7e500bf8c Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Wed, 24 Jul 2019 17:18:15 -0400
Subject: view specsss

---
 package.json                                       |   1 +
 src/client/views/EditableView.tsx                  |   2 +-
 src/client/views/MainView.tsx                      |   4 +-
 .../views/collections/CollectionStackingView.tsx   |   9 +-
 .../CollectionStackingViewFieldColumn.tsx          |  44 +++-
 src/client/views/collections/CollectionSubView.tsx |  15 +-
 .../views/collections/CollectionViewChromes.scss   | 113 ++++++++-
 .../views/collections/CollectionViewChromes.tsx    | 261 ++++++++++++++++-----
 src/client/views/collections/KeyRestrictionRow.tsx |  45 ++++
 9 files changed, 428 insertions(+), 66 deletions(-)
 create mode 100644 src/client/views/collections/KeyRestrictionRow.tsx

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

diff --git a/package.json b/package.json
index 4a15cbb2f..37052fde3 100644
--- a/package.json
+++ b/package.json
@@ -136,6 +136,7 @@
         "image-size": "^0.7.4",
         "imagesloaded": "^4.1.4",
         "jquery-awesome-cursor": "^0.3.1",
+        "js-datepicker": "^4.6.6",
         "jsonwebtoken": "^8.5.0",
         "jsx-to-string": "^1.4.0",
         "lodash": "^4.17.11",
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index a5bb40243..bd00d47b9 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -116,7 +116,7 @@ export class EditableView extends React.Component<EditableProps> {
             return (
                 <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
                     style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
-                    onClick={this.onClick} >
+                    onClick={this.onClick}>
                     <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span>
                 </div>
             );
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 94a4835a1..69602deed 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, faCaretUp, faLongArrowAltRight, 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
 import { observer } from 'mobx-react';
@@ -127,7 +127,9 @@ export class MainView extends React.Component {
         library.add(faCut);
         library.add(faCommentAlt);
         library.add(faThumbtack);
+        library.add(faLongArrowAltRight);
         library.add(faCheck);
+        library.add(faCaretUp);
         library.add(faArrowDown);
         library.add(faArrowUp);
         library.add(faCloudUploadAlt);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index e4e6f92e3..a78a47ffb 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -249,6 +249,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         return false;
     }
 
+    sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
+        let descending = BoolCast(this.props.Document.stackingHeadersSortDescending);
+        let firstEntry = descending ? b : a;
+        let secondEntry = descending ? a : b;
+        return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
+    }
+
     render() {
         let headings = Array.from(this.Sections.keys());
         let editableViewProps = {
@@ -264,7 +271,7 @@ 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()).sort((a, b) => a[0].toString() > b[0].toString() ? 1 : -1).
+                {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc).
                     map(section => this.section(section[0], section[1] as Doc[])) :
                     this.section(undefined, this.filteredChildren)}
                 {this.props.Document.sectionFilter ?
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index fe24c63c7..582adc418 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -16,6 +16,8 @@ import "./CollectionStackingView.scss";
 import { Docs } from "../../documents/Documents";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CompileScript } from "../../util/Scripting";
 
 
 interface CSVFieldColumnProps {
@@ -35,6 +37,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
 
     private _dropRef: HTMLDivElement | null = null;
     private dropDisposer?: DragManager.DragDropDisposer;
+    private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
 
     @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
 
@@ -182,6 +185,43 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         }
     }
 
+    startDrag = (e: PointerEvent) => {
+        let alias = Doc.MakeAlias(this.props.parent.props.Document);
+        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        let value = this.getValue(this._heading);
+        value = typeof value === "string" ? `"${value}"` : value;
+        let script = `return doc.${key} === ${value}`;
+        let compiled = CompileScript(script, { params: { doc: Doc.name } });
+        if (compiled.compiled) {
+            let scriptField = new ScriptField(compiled);
+            alias.viewSpecScript = scriptField;
+            let dragData = new DragManager.DocumentDragData([alias], [alias.proto]);
+            DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
+        }
+
+        e.stopPropagation();
+        document.removeEventListener("pointermove", this.startDrag);
+        document.removeEventListener("pointerup", this.pointerUp);
+    }
+
+    pointerUp = (e: PointerEvent) => {
+        e.stopPropagation();
+        e.preventDefault();
+
+        document.removeEventListener("pointermove", this.startDrag);
+        document.removeEventListener("pointerup", this.pointerUp);
+    }
+
+    headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+        e.stopPropagation();
+        e.preventDefault();
+
+        document.removeEventListener("pointermove", this.startDrag);
+        document.addEventListener("pointermove", this.startDrag);
+        document.removeEventListener("pointerup", this.pointerUp);
+        document.addEventListener("pointerup", this.pointerUp);
+    }
+
     render() {
         let cols = this.props.cols();
         let key = StrCast(this.props.parent.props.Document.sectionFilter);
@@ -199,11 +239,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             oneLine: true
         }
         let headingView = this.props.headingObject ?
-            <div key={heading} className="collectionStackingView-sectionHeader"
+            <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
                 style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
                     Further, it does not have a color and cannot be deleted. */}
-                <div className="collectionStackingView-sectionHeader-subCont"
+                <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
                     title={evContents === `No ${key} value` ?
                         `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
                     style={{
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 2ddefb3c0..24c8adb35 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -21,6 +21,8 @@ import { CollectionView } from "./CollectionView";
 import React = require("react");
 import { MainView } from "../MainView";
 import { Utils } from "../../../Utils";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CompileScript } from "../../util/Scripting";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -54,7 +56,18 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             let self = this;
             //TODO tfs: This might not be what we want?
             //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
-            return DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+            let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+            let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+            if (viewSpecScript) {
+                let script = viewSpecScript.script;
+                docs = docs.filter(d => {
+                    let res = script.run({ doc: d });
+                    if (res.success) {
+                        return res.result;
+                    }
+                });
+            }
+            return docs;
         }
         get childDocList() {
             //TODO tfs: This might not be what we want?
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index e5cb1b546..3103cd309 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -1,12 +1,120 @@
 @import "../globalCssVariables";
+@import '~js-datepicker/dist/datepicker.min.css';
+
+.collectionViewBaseChrome {
+    display: flex;
+
+    .collectionViewBaseChrome-viewPicker {
+        font-size: 75%;
+        text-transform: uppercase;
+        letter-spacing: 2px;
+        background: rgb(238, 238, 238);
+        color: grey;
+        outline-color: black;
+        border: none;
+        padding: 12px 10px 11px 10px;
+    }
+
+    .collectionViewBaseChrome-viewPicker:active {
+        outline-color: black;
+    }
+
+    .collectionViewBaseChrome-collapse {
+        margin-right: 10px;
+        transition: all .5s;
+    }
+
+    .collectionViewBaseChrome-viewSpecs {
+        margin-left: 10px;
+        display: grid;
+
+        .collectionViewBaseChrome-viewSpecsInput {
+            padding: 12px 10px 11px 10px;
+            border: 0px;
+            color: grey;
+            text-align: center;
+            text-transform: uppercase;
+            letter-spacing: 2px;
+            outline-color: black;
+            font-size: 75%;
+            background: rgb(238, 238, 238);
+            height: 100%;
+            width: 150px;
+        }
+
+        .collectionViewBaseChrome-viewSpecsMenu {
+            overflow: hidden;
+            transition: height .5s, display .5s;
+            position: absolute;
+            top: 60px;
+            z-index: 100;
+            display: flex;
+            flex-direction: column;
+            background: rgb(238, 238, 238);
+            box-shadow: grey 2px 2px 4px;
+
+            .qs-datepicker {
+                left: unset;
+                right: 0;
+            }
+
+            .collectionViewBaseChrome-viewSpecsMenu-row {
+                display: grid;
+                grid-template-columns: 150px 200px 150px;
+                margin-top: 10px;
+                margin-right: 10px;
+
+                .collectionViewBaseChrome-viewSpecsMenu-rowLeft,
+                .collectionViewBaseChrome-viewSpecsMenu-rowMiddle,
+                .collectionViewBaseChrome-viewSpecsMenu-rowRight {
+                    font-size: 75%;
+                    letter-spacing: 2px;
+                    color: grey;
+                    margin-left: 10px;
+                    padding: 5px;
+                    border: none;
+                    outline-color: black;
+                }
+            }
+
+            .collectionViewBaseChrome-viewSpecsMenu-lastRow {
+                display: grid;
+                grid-template-columns: 1fr 1fr;
+                grid-gap: 10px;
+                margin: 10px;
+            }
+        }
+    }
+}
 
 .collectionStackingViewChrome {
     display: grid;
-    grid-template-columns: 1fr 1fr;
+    grid-template-columns: 1fr auto;
     padding-bottom: 10px;
     border-bottom: .5px solid lightgrey;
     margin: 10px;
 
+    .collectionStackingViewChrome-cont {
+        display: flex;
+        justify-content: space-between;
+    }
+
+    .collectionStackingViewChrome-sort {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .collectionStackingViewChrome-sortIcon {
+            transition: transform .5s;
+            margin-left: 10px;
+        }
+    }
+
+    button:hover {
+        transform: scale(1);
+    }
+
+
     .collectionStackingViewChrome-sectionFilter-cont {
         justify-self: right;
         display: flex;
@@ -21,10 +129,9 @@
 
         .collectionStackingViewChrome-sectionFilter {
             color: white;
-            width: fit-content;
+            width: 100px;
             text-align: center;
             background: rgb(238, 238, 238);
-            height: 37px;
 
             .editable-view-input,
             input,
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 52fee26bf..2edac384d 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -3,55 +3,189 @@ import { CollectionView } from "./CollectionView";
 import "./CollectionViewChromes.scss";
 import { CollectionViewType } from "./CollectionBaseView";
 import { undoBatch } from "../../util/UndoManager";
-import { action, observable, runInAction, computed } from "mobx";
+import { action, observable, runInAction, computed, IObservable, IObservableValue } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { DocLike } from "../MetadataEntryMenu";
-const higflyout = require("@hig/flyout");
-export const Flyout = higflyout.default;
 import * as Autosuggest from 'react-autosuggest';
 import { EditableView } from "../EditableView";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, NumCast, BoolCast, Cast } from "../../../new_fields/Types";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Utils } from "../../../Utils";
+import KeyRestrictionRow from "./KeyRestrictionRow";
+import { CompileScript } from "../../util/Scripting";
+import { ScriptField } from "../../../new_fields/ScriptField";
+const datepicker = require('js-datepicker');
 
 interface CollectionViewChromeProps {
     CollectionView: CollectionView;
 }
 
+let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
+
+@observer
 class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
+    @observable private _viewSpecsOpen: boolean = false;
+    @observable private _dateWithinValue: string = "";
+    @observable private _dateValue: Date = new Date();
+    @observable private _keyRestrictions: [JSX.Element, string][] = [];
+    @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); }
+
+    private _picker: any;
+    private _datePickerElGuid = Utils.GenerateGuid();
+
+    componentDidMount = () => {
+        this._picker = datepicker("#" + this._datePickerElGuid, {
+            disabler: (date: Date) => date > new Date(),
+            onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date),
+            dateSelected: new Date()
+        });
+
+        runInAction(() => {
+            this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]);
+            this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={false} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]);
+        });
+    }
+
     @undoBatch
     viewChanged = (e: React.ChangeEvent) => {
         //@ts-ignore
-        switch (e.target.selectedOptions[0].value) {
-            case "freeform":
-                this.props.CollectionView.props.Document.viewType = CollectionViewType.Freeform;
-                break;
-            case "schema":
-                this.props.CollectionView.props.Document.viewType = CollectionViewType.Schema;
-                break;
-            case "treeview":
-                this.props.CollectionView.props.Document.viewType = CollectionViewType.Tree;
-                break;
-            case "stacking":
-                this.props.CollectionView.props.Document.viewType = CollectionViewType.Stacking;
-                break;
-            case "masonry":
-                this.props.CollectionView.props.Document.viewType = CollectionViewType.Masonry;
-                break;
-            default:
-                break;
+        this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value);
+    }
+
+    @action
+    openViewSpecs = (e: React.SyntheticEvent) => {
+        this._viewSpecsOpen = true;
+
+        //@ts-ignore
+        if (!e.target.classList[0].startsWith("qs")) {
+            this.closeDatePicker();
+        }
+
+        e.stopPropagation();
+        document.removeEventListener("pointerdown", this.closeViewSpecs);
+        document.addEventListener("pointerdown", this.closeViewSpecs);
+    }
+
+    @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); };
+
+    @action
+    openDatePicker = (e: React.PointerEvent) => {
+        this.openViewSpecs(e);
+        if (this._picker) {
+            this._picker.alwaysShow = true;
+            this._picker.show();
+            // TODO: calendar is offset when zoomed in/out
+            // this._picker.calendar.style.position = "absolute";
+            // let transform = this.props.CollectionView.props.ScreenToLocalTransform();
+            // let x = parseInt(this._picker.calendar.style.left) / transform.Scale;
+            // let y = parseInt(this._picker.calendar.style.top) / transform.Scale;
+            // this._picker.calendar.style.left = x;
+            // this._picker.calendar.style.top = y;
+
+            e.stopPropagation();
         }
     }
 
+    @action
+    addKeyRestriction = (e: React.MouseEvent) => {
+        let index = this._keyRestrictions.length;
+        this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]);
+
+        this.openViewSpecs(e);
+    }
+
+    @action
+    applyFilter = (e: React.MouseEvent) => {
+        this.openViewSpecs(e);
+
+        let keyRestrictionScript = `${this._keyRestrictions.map(i => i[1])
+            .reduce((acc: string, value: string, i: number) => value ? `${acc} && ${value}` : acc)}`;
+        let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
+        let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
+        let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
+        let dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7;
+        let lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset);
+        let upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1);
+        let dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`;
+        let fullScript = `return ${dateRestrictionScript} && ${keyRestrictionScript}`;
+        let compiled = CompileScript(fullScript, { params: { doc: Doc.name } });
+        if (compiled.compiled) {
+            this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
+        }
+    }
+
+    @action
+    closeDatePicker = () => {
+        if (this._picker) {
+            this._picker.alwaysShow = false;
+            this._picker.hide();
+        }
+        document.removeEventListener("pointerdown", this.closeDatePicker);
+    }
+
     render() {
         return (
             <div className="collectionViewBaseChrome">
-                <select onChange={this.viewChanged}>
-                    <option value="freeform" selected={this.props.CollectionView.props.Document.viewType === CollectionViewType.Freeform}>Freeform View</option>
-                    <option value="schema" selected={this.props.CollectionView.props.Document.viewType === CollectionViewType.Schema}>Schema View</option>
-                    <option value="treeview" selected={this.props.CollectionView.props.Document.viewType === CollectionViewType.Tree}>Tree View</option>
-                    <option value="stacking" selected={this.props.CollectionView.props.Document.viewType === CollectionViewType.Stacking}>Stacking View</option>
-                    <option value="masonry" selected={this.props.CollectionView.props.Document.viewType === CollectionViewType.Masonry}>Masonry View</option>
+                <button className="collectionViewBaseChrome-collapse" title="Collapse collection chrome">
+                    <FontAwesomeIcon icon="caret-up" size="2x" />
+                </button>
+                <select
+                    className="collectionViewBaseChrome-viewPicker"
+                    onPointerDown={stopPropagation}
+                    onChange={this.viewChanged}
+                    value={NumCast(this.props.CollectionView.props.Document.viewType)}>
+                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option>
+                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option>
+                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
+                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
+                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View></option>
                 </select>
+                <div className="collectionViewBaseChrome-viewSpecs">
+                    <input className="collectionViewBaseChrome-viewSpecsInput"
+                        placeholder="Filter Documents"
+                        value={this.filterValue ? this.filterValue.script.originalScript : ""}
+                        onPointerDown={this.openViewSpecs} />
+                    <div className="collectionViewBaseChrome-viewSpecsMenu"
+                        onPointerDown={this.openViewSpecs}
+                        style={{
+                            height: this._viewSpecsOpen ? "fit-content" : "0px",
+                            overflow: this._viewSpecsOpen ? "initial" : "hidden"
+                        }}>
+                        {this._keyRestrictions.map(i => i[0])}
+                        <div className="collectionViewBaseChrome-viewSpecsMenu-row">
+                            <div className="collectionViewBaseChrome-viewSpecsMenu-rowLeft">
+                                CREATED WITHIN:
+                            </div>
+                            <select className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
+                                style={{ textTransform: "uppercase", textAlign: "center" }}
+                                value={this._dateWithinValue}
+                                onChange={(e) => runInAction(() => this._dateWithinValue = e.target.value)}>
+                                <option value="1d">1 day of</option>
+                                <option value="3d">3 days of</option>
+                                <option value="1w">1 week of</option>
+                                <option value="2w">2 weeks of</option>
+                                <option value="1m">1 month of</option>
+                                <option value="2m">2 months of</option>
+                                <option value="6m">6 months of</option>
+                                <option value="1y">1 year of</option>
+                            </select>
+                            <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
+                                id={this._datePickerElGuid}
+                                value={this._dateValue.toLocaleDateString()}
+                                onPointerDown={this.openDatePicker}
+                                placeholder="Value" />
+                        </div>
+                        <div className="collectionViewBaseChrome-viewSpecsMenu-lastRow">
+                            <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.addKeyRestriction}>
+                                ADD KEY RESTRICTION
+                            </button>
+                            <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.applyFilter}>
+                                APPLY FILTER
+                            </button>
+                        </div>
+                    </div>
+                </div>
             </div>
         )
     }
@@ -62,6 +196,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
     @observable private _currentKey: string = "";
     @observable private suggestions: string[] = [];
 
+    @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); }
     @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
 
     getKeySuggestions = async (value: string): Promise<string[]> => {
@@ -109,41 +244,53 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
         return true;
     }
 
+    @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }
     @action resetValue = () => { this._currentKey = this.sectionFilter; };
 
     render() {
         return (
             <div className="collectionStackingViewChrome">
                 <CollectionViewBaseChrome CollectionView={this.props.CollectionView} />
-                <div className="collectionStackingViewChrome-sectionFilter-cont">
-                    <div className="collectionStackingViewChrome-sectionFilter-label">
-                        Group items by:
-                    </div>
-                    <div className="collectionStackingViewChrome-sectionFilter">
-                        <EditableView
-                            GetValue={() => this.sectionFilter}
-                            autosuggestProps={
-                                {
-                                    resetValue: this.resetValue,
-                                    value: this._currentKey,
-                                    onChange: this.onKeyChange,
-                                    autosuggestProps: {
-                                        inputProps:
-                                        {
-                                            value: this._currentKey,
-                                            onChange: this.onKeyChange
-                                        },
-                                        getSuggestionValue: this.getSuggestionValue,
-                                        suggestions: this.suggestions,
-                                        alwaysRenderSuggestions: true,
-                                        renderSuggestion: this.renderSuggestion,
-                                        onSuggestionsFetchRequested: this.onSuggestionFetch,
-                                        onSuggestionsClearRequested: this.onSuggestionClear
-                                    }
-                                }}
-                            SetValue={this.setValue}
-                            contents={this.sectionFilter ? this.sectionFilter : "N/A"}
-                        />
+                <div className="collectionStackingViewChrome-cont">
+                    <button className="collectionStackingViewChrome-sort" onClick={this.toggleSort}>
+                        <div className="collectionStackingViewChrome-sortLabel">
+                            Sort
+                        </div>
+                        <div className="collectionStackingViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+                            <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+                        </div>
+                    </button>
+                    <div className="collectionStackingViewChrome-sectionFilter-cont">
+                        <div className="collectionStackingViewChrome-sectionFilter-label">
+                            Group items by:
+                        </div>
+                        <div className="collectionStackingViewChrome-sectionFilter">
+                            <EditableView
+                                GetValue={() => this.sectionFilter}
+                                autosuggestProps={
+                                    {
+                                        resetValue: this.resetValue,
+                                        value: this._currentKey,
+                                        onChange: this.onKeyChange,
+                                        autosuggestProps: {
+                                            inputProps:
+                                            {
+                                                value: this._currentKey,
+                                                onChange: this.onKeyChange
+                                            },
+                                            getSuggestionValue: this.getSuggestionValue,
+                                            suggestions: this.suggestions,
+                                            alwaysRenderSuggestions: true,
+                                            renderSuggestion: this.renderSuggestion,
+                                            onSuggestionsFetchRequested: this.onSuggestionFetch,
+                                            onSuggestionsClearRequested: this.onSuggestionClear
+                                        }
+                                    }}
+                                oneLine
+                                SetValue={this.setValue}
+                                contents={this.sectionFilter ? this.sectionFilter : "N/A"}
+                            />
+                        </div>
                     </div>
                 </div>
             </div>
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
new file mode 100644
index 000000000..8051a8359
--- /dev/null
+++ b/src/client/views/collections/KeyRestrictionRow.tsx
@@ -0,0 +1,45 @@
+import * as React from "react";
+import { observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
+
+interface IKeyRestrictionProps {
+    contains: boolean;
+    script: (value: string) => void;
+}
+
+@observer
+export default class KeyRestrictionRow extends React.Component<IKeyRestrictionProps> {
+    @observable private _key = "";
+    @observable private _value = "";
+    @observable private _contains = this.props.contains;
+
+    render() {
+        if (this._key && this._value) {
+            let parsedValue: string | number = `"${this._value}"`;
+            let parsed = parseInt(this._value);
+            if (!isNaN(parsed)) {
+                parsedValue = parsed;
+            }
+            let scriptText = `(doc.${this._key} ${this._contains ? "===" : "!=="} ${parsedValue})`;
+            this.props.script(scriptText);
+        }
+        return (
+            <div className="collectionViewBaseChrome-viewSpecsMenu-row">
+                <input className="collectionViewBaseChrome-viewSpecsMenu-rowLeft"
+                    value={this._key}
+                    onChange={(e) => runInAction(() => this._key = e.target.value)}
+                    placeholder="KEY" />
+                <button className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
+                    style={{ background: PastelSchemaPalette.get(this._contains ? "green" : "red") }}
+                    onClick={() => runInAction(() => this._contains = !this._contains)}>
+                    {this._contains ? "CONTAINS" : "DOES NOT CONTAIN"}
+                </button>
+                <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
+                    value={this._value}
+                    onChange={(e) => runInAction(() => this._value = e.target.value)}
+                    placeholder="VALUE" />
+            </div>
+        )
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 0baefb4e0b133df60f42d894733113c961740af3 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Thu, 25 Jul 2019 19:30:44 -0400
Subject: almost done

---
 .../views/collections/CollectionStackingView.scss  | 42 ++++++++++++----------
 .../views/collections/CollectionStackingView.tsx   |  8 ++---
 .../CollectionStackingViewFieldColumn.tsx          | 26 ++++++++------
 .../views/collections/CollectionViewChromes.scss   |  5 +--
 .../views/collections/CollectionViewChromes.tsx    |  6 ++--
 5 files changed, 48 insertions(+), 39 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index f43340967..9dbe4ccb8 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -96,7 +96,6 @@
             outline: none;
             border: 0px;
             color: $light-color;
-            text-transform: uppercase;
             letter-spacing: 2px;
             font-size: 75%;
             transition: transform 0.2s;
@@ -118,9 +117,7 @@
                 padding: 12px 10px 11px 10px;
                 border: 0px;
                 color: grey;
-                // font-size: 75%;
                 text-align: center;
-                text-transform: uppercase;
                 letter-spacing: 2px;
                 outline-color: black;
             }
@@ -142,15 +139,6 @@
         width: 90%;
         color: lightgrey;
         overflow: ellipses;
-    }
-
-    .collectionStackingView-addGroupButton {
-        background: rgb(238, 238, 238);
-        font-size: 75%;
-        text-align: center;
-        text-transform: uppercase;
-        letter-spacing: 2px;
-        height: fit-content;
 
         .editableView-container-editing-oneLine,
         .editableView-container-editing {
@@ -165,14 +153,32 @@
         }
 
         .editableView-input {
-            padding: 12px 10px 11px 10px;
-            border: 0px;
-            color: grey;
-            // font-size: 75%;
-            text-align: center;
-            text-transform: uppercase;
+            outline-color: black;
             letter-spacing: 2px;
+            color: grey;
+            border: 0px;
+            padding: 12px 10px 11px 10px;
+        }
+    }
+
+    .collectionStackingView-addDocumentButton {
+        font-size: 75%;
+        letter-spacing: 2px;
+
+        .editableView-input {
             outline-color: black;
+            letter-spacing: 2px;
+            color: grey;
+            border: 0px;
+            padding: 12px 10px 11px 10px;
         }
     }
+
+    .collectionStackingView-addGroupButton {
+        background: rgb(238, 238, 238);
+        font-size: 75%;
+        text-align: center;
+        letter-spacing: 2px;
+        height: fit-content;
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index b5c0fb3d6..7c93201fe 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -47,7 +47,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []]));
         if (sectionFilter) {
             this.filteredChildren.map(d => {
-                let sectionValue = (d[sectionFilter] ? d[sectionFilter] : `No ${sectionFilter} value`) as object;
+                let sectionValue = (d[sectionFilter] ? d[sectionFilter] : `NO ${sectionFilter.toUpperCase()} VALUE`) as object;
                 // the next five lines ensures that floating point rounding errors don't create more than one section -syip
                 let parsed = parseInt(sectionValue.toString());
                 let castedSectionValue: any = sectionValue;
@@ -56,12 +56,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                 }
 
                 // look for if header exists already
-                let existingHeader = sectionHeaders!.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `No ${sectionFilter} value`));
+                let existingHeader = sectionHeaders!.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${sectionFilter.toUpperCase()} VALUE`));
                 if (existingHeader) {
                     fields.get(existingHeader)!.push(d);
                 }
                 else {
-                    let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `No ${sectionFilter} value`);
+                    let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${sectionFilter.toUpperCase()} VALUE`);
                     fields.set(newSchemaHeader, [d]);
                     sectionHeaders!.push(newSchemaHeader);
                 }
@@ -260,7 +260,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         let editableViewProps = {
             GetValue: () => "",
             SetValue: this.addGroup,
-            contents: "+ Add a Group"
+            contents: "+ ADD A GROUP"
         }
         // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
         return (
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 582adc418..ea2a302ff 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -18,6 +18,7 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { CompileScript } from "../../util/Scripting";
+import { RichTextField } from "../../../new_fields/RichTextField";
 
 
 interface CSVFieldColumnProps {
@@ -168,11 +169,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     }
 
     @action
-    addDocument = () => {
+    addDocument = (value: string, shiftDown?: boolean) => {
         let key = StrCast(this.props.parent.props.Document.sectionFilter);
-        let newDoc = Docs.Create.TextDocument({ height: 18, title: "new text document" });
+        let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
         newDoc[key] = this.getValue(this.props.heading);
-        this.props.parent.props.addDocument(newDoc);
+        return this.props.parent.props.addDocument(newDoc);
     }
 
     @action
@@ -231,29 +232,34 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         let style = this.props.parent;
         let singleColumn = style.singleColumn;
         let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
-        let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `No ${key} value`;
-        let editableViewProps = {
+        let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+        let headerEditableViewProps = {
             GetValue: () => evContents,
             SetValue: this.headingChanged,
             contents: evContents,
             oneLine: true
         }
+        let newEditableViewProps = {
+            GetValue: () => "",
+            SetValue: this.addDocument,
+            contents: "+ NEW"
+        }
         let headingView = this.props.headingObject ?
             <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
                 style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
                     Further, it does not have a color and cannot be deleted. */}
                 <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
-                    title={evContents === `No ${key} value` ?
+                    title={evContents === `NO ${key.toUpperCase()} VALUE` ?
                         `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
                     style={{
                         width: "100%",
-                        background: this.props.headingObject && evContents !== `No ${key} value` ?
+                        background: this.props.headingObject && evContents !== `NO ${key.toUpperCase()} VALUE` ?
                             this.props.headingObject.color : "lightgrey",
                         color: "grey"
                     }}>
-                    <EditableView {...editableViewProps} />
-                    {evContents === `No ${key} value` ?
+                    <EditableView {...headerEditableViewProps} />
+                    {evContents === `NO ${key.toUpperCase()} VALUE` ?
                         (null) :
                         <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
                             <FontAwesomeIcon icon="trash" />
@@ -282,7 +288,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 </div>
                 <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
                     style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}>
-                    <button style={{ width: "100%" }} onClick={this.addDocument}>+ New</button>
+                    <EditableView {...newEditableViewProps} />
                 </div>
             </div>
         );
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 0faca0607..6525f3b07 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -5,7 +5,7 @@
     position: relative;
     z-index: 9001;
     transition: top .5s;
-    background: rgb(238, 238, 238);
+    background: lightslategray;
     padding: 10px;
 
     .collectionViewChrome {
@@ -48,7 +48,6 @@
                     border: 0px;
                     color: grey;
                     text-align: center;
-                    text-transform: uppercase;
                     letter-spacing: 2px;
                     outline-color: black;
                     font-size: 75%;
@@ -128,7 +127,6 @@
             justify-self: right;
             display: flex;
             font-size: 75%;
-            text-transform: uppercase;
             letter-spacing: 2px;
 
             .collectionStackingViewChrome-sectionFilter-label {
@@ -150,7 +148,6 @@
                     border: 0px;
                     color: grey;
                     text-align: center;
-                    text-transform: uppercase;
                     letter-spacing: 2px;
                     outline-color: black;
                     height: 100%;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 4129781f7..78ceaad86 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -180,11 +180,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
-                            <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View></option>
+                            <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
                         </select>
                         <div className="collectionViewBaseChrome-viewSpecs">
                             <input className="collectionViewBaseChrome-viewSpecsInput"
-                                placeholder="Filter Documents"
+                                placeholder="FILTER DOCUMENTS"
                                 value={this.filterValue ? this.filterValue.script.originalScript : ""}
                                 onPointerDown={this.openViewSpecs} />
                             <div className="collectionViewBaseChrome-viewSpecsMenu"
@@ -304,7 +304,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
                 </button>
                 <div className="collectionStackingViewChrome-sectionFilter-cont">
                     <div className="collectionStackingViewChrome-sectionFilter-label">
-                        Group items by:
+                        GROUP ITEMS BY:
                         </div>
                     <div className="collectionStackingViewChrome-sectionFilter">
                         <EditableView
-- 
cgit v1.2.3-70-g09d2


From 73fcf938eb817ae9b4dfdcefd5f04502079c153e Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sat, 27 Jul 2019 18:38:09 -0400
Subject: Fixed linter errors

---
 src/client/views/collections/CollectionStackingViewFieldColumn.tsx | 6 +++---
 src/client/views/collections/CollectionViewChromes.tsx             | 2 +-
 src/client/views/collections/KeyRestrictionRow.tsx                 | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 1a5d203f9..cf6079196 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -74,7 +74,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             let layoutDoc = Doc.expandTemplateLayout(d, parent.props.DataDoc);
             let headings = this.props.headings();
             let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
-            let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d)
+            let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
             let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth) : parent.columnWidth) / (uniqueHeadings.length + 1);
             let height = () => parent.getDocHeight(pair.layout);
             if (parent.singleColumn) {
@@ -243,12 +243,12 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             SetValue: this.headingChanged,
             contents: evContents,
             oneLine: true
-        }
+        };
         let newEditableViewProps = {
             GetValue: () => "",
             SetValue: this.addDocument,
             contents: "+ NEW"
-        }
+        };
         let headingView = this.props.headingObject ?
             <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
                 style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}>
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index de1d01ef0..bf643347f 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -296,7 +296,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
         return true;
     }
 
-    @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }
+    @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; };
     @action resetValue = () => { this._currentKey = this.sectionFilter; };
 
     render() {
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
index 8051a8359..f292557b5 100644
--- a/src/client/views/collections/KeyRestrictionRow.tsx
+++ b/src/client/views/collections/KeyRestrictionRow.tsx
@@ -40,6 +40,6 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr
                     onChange={(e) => runInAction(() => this._value = e.target.value)}
                     placeholder="VALUE" />
             </div>
-        )
+        );
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From dd69e93fcfebebf50f218fc8c9f1746e6c5a3be8 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sun, 28 Jul 2019 15:13:08 -0400
Subject: fixed doc decoration merge loss

---
 .../CollectionStackingViewFieldColumn.tsx          | 74 +++++++++-------------
 1 file changed, 31 insertions(+), 43 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index cf6079196..cf5bc451a 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -69,62 +69,50 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
 
     children(docs: Doc[]) {
         let parent = this.props.parent;
-        this.props.parent._docXfs.length = 0;
+        parent._docXfs.length = 0;
         return docs.map((d, i) => {
-            let layoutDoc = Doc.expandTemplateLayout(d, parent.props.DataDoc);
             let headings = this.props.headings();
             let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
             let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
             let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth) : parent.columnWidth) / (uniqueHeadings.length + 1);
             let height = () => parent.getDocHeight(pair.layout);
-            if (parent.singleColumn) {
-                let dxf;
-                let dref = React.createRef<HTMLDivElement>();
-                if (uniqueHeadings.length > 0) {
-                    dxf = () => this.getDocTransform(layoutDoc, dref.current!);
-                    this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
-                }
-                else {
-                    //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
-                    dxf = () => this.getSingleDocTransform(layoutDoc, i, width());
-                    this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
-                }
-                let rowHgtPcnt = height();
-                return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} style={{ width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` }} >
-                    {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
-                </div>;
-            } else {
-                let dref = React.createRef<HTMLDivElement>();
-                let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
-                this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
-                let rowHgtPcnt = height();
-                let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
-                let divStyle = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
-                return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={divStyle} >
-                    {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
-                </div>;
-            }
+            let dref = React.createRef<HTMLDivElement>();
+            // if (uniqueHeadings.length > 0) {
+            let dxf = () => this.getDocTransform(pair.layout, dref.current!);
+            this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+            // }
+            // else {
+            //     //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
+            //     let dxf = () => this.getDocTransform(layoutDoc, i, width());
+            //     this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+            // }
+            let rowHgtPcnt = height();
+            let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
+            let style = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
+            return <div className={`collectionStackingView-${parent.singleColumn ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
+                {this.props.parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
+            </div>;
+            // } else {
+            // let dref = React.createRef<HTMLDivElement>();
+            // let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+            // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+            // let rowHgtPcnt = height();
+            // let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
+            // let divStyle = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
+            // return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={divStyle} >
+            //     {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
+            // </div>;
+            // }
         });
     }
 
-    getSingleDocTransform(doc: Doc, ind: number, width: number) {
-        let localY = this.props.parent.filteredChildren.reduce((height, d, i) =>
-            height + (i < ind ? this.props.parent.getDocHeight(Doc.expandTemplateLayout(d, this.props.parent.props.DataDoc)) + this.props.parent.gridGap : 0), this.props.parent.yMargin);
-        let translate = this.props.parent.props.ScreenToLocalTransform().inverse().transformPoint((this.props.parent.props.PanelWidth() - width) / 2, localY);
-        return this.offsetTransform(doc, translate[0], translate[1]);
-    }
-
-    offsetTransform(doc: Doc, translateX: number, translateY: number) {
-        let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
-        let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
-        return this.props.parent.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
-    }
-
     getDocTransform(doc: Doc, dref: HTMLDivElement) {
         let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
         let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
         let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
-        return this.props.parent.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
+        return this.props.parent.props.ScreenToLocalTransform().
+            translate(offset[0], offset[1] - (this.props.parent.chromeCollapsed ? 0 : 100)).
+            scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
     }
 
     getValue = (value: string): any => {
-- 
cgit v1.2.3-70-g09d2


From e7ea2028f54787d6c92fb22b789f17b7268d3793 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 29 Jul 2019 08:37:53 -0400
Subject: some layout adjustments for stacking views

---
 src/client/views/collections/CollectionStackingView.tsx        |  4 ++--
 .../views/collections/CollectionStackingViewFieldColumn.tsx    | 10 +++++-----
 src/client/views/nodes/DocumentView.tsx                        |  2 +-
 3 files changed, 8 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index b96b1f8c8..f647da8f0 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -140,12 +140,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             previewScript={undefined}>
         </CollectionSchemaPreview>;
     }
-    getDocHeight(d: Doc) {
+    getDocHeight(d: Doc, columnScale: number = 1) {
         let nw = NumCast(d.nativeWidth);
         let nh = NumCast(d.nativeHeight);
         if (!BoolCast(d.ignoreAspect) && nw && nh) {
             let aspect = nw && nh ? nh / nw : 1;
-            let wid = Math.min(d[WidthSym](), this.columnWidth);
+            let wid = Math.min(d[WidthSym](), this.columnWidth / columnScale);
             return wid * aspect;
         }
         return d[HeightSym]();
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index cf5bc451a..387e189e7 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -74,8 +74,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             let headings = this.props.headings();
             let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
             let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
-            let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth) : parent.columnWidth) / (uniqueHeadings.length + 1);
-            let height = () => parent.getDocHeight(pair.layout);
+            let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth / (uniqueHeadings.length + 1)) : parent.columnWidth / (uniqueHeadings.length + 1));/// (uniqueHeadings.length + 1);
+            let height = () => parent.getDocHeight(pair.layout, uniqueHeadings.length + 1);// / (d.nativeWidth && !BoolCast(d.ignoreAspect) ? uniqueHeadings.length + 1 : 1);
             let dref = React.createRef<HTMLDivElement>();
             // if (uniqueHeadings.length > 0) {
             let dxf = () => this.getDocTransform(pair.layout, dref.current!);
@@ -88,7 +88,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             // }
             let rowHgtPcnt = height();
             let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
-            let style = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
+            let style = parent.singleColumn ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
             return <div className={`collectionStackingView-${parent.singleColumn ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
                 {this.props.parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
             </div>;
@@ -267,8 +267,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
                     style={{
                         padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
-                        margin: "auto 5px",
-                        width: singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+                        margin: "auto",
+                        width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
                         height: 'max-content',
                         position: "relative",
                         gridGap: style.gridGap,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 99f073459..f101222ae 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -716,7 +716,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                                 transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`
                             }}>
                                 <EditableView
-                                    contents={(this.dataDoc || this.layoutDoc)[showTitle]}
+                                    contents={this.layoutDoc[showTitle]}
                                     display={"block"}
                                     height={72}
                                     fontSize={12}
-- 
cgit v1.2.3-70-g09d2

