diff options
Diffstat (limited to 'src')
4 files changed, 266 insertions, 58 deletions
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`; + } +} + |