diff options
author | yipstanley <stanley_yip@brown.edu> | 2019-07-19 17:01:52 -0400 |
---|---|---|
committer | yipstanley <stanley_yip@brown.edu> | 2019-07-19 17:01:52 -0400 |
commit | 8c266048054dccc20851f79b08c82aa552765158 (patch) | |
tree | 19106491ede918ccafa2a3c2be4a458499e9cc67 /src | |
parent | e4ef3a04e3bd0bb20ed70181d51a9c419a00613f (diff) | |
parent | 17f53f604e0087615c2baff6cffa344771301b5e (diff) |
Merge branch 'schema_view_improvements_2' of https://github.com/browngraphicslab/Dash-Web into schema_view_improvements_2
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/DragManager.ts | 13 | ||||
-rw-r--r-- | src/client/views/EditableView.tsx | 12 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaCells.tsx | 8 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaHeaders.tsx | 86 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaMovableTableHOC.tsx | 193 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaView.scss | 84 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaView.tsx | 262 |
7 files changed, 499 insertions, 159 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 323908302..f9f6b05c0 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -288,6 +288,15 @@ export namespace DragManager { [id: string]: any; } + // for column dragging in schema view + export class ColumnDragData { + constructor(colKey: string) { + this.colKey = colKey; + } + colKey: string; + [id: string]: any; + } + export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) { StartDrag([ele], dragData, downX, downY, options); } @@ -296,6 +305,10 @@ export namespace DragManager { StartDrag([ele], dragData, downX, downY, options); } + export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?:DragOptions) { + StartDrag([ele], dragData, downX, downY, options); + } + export let AbortDrag: () => void = emptyFunction; function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 989fb1be9..42faedf9d 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -31,6 +31,7 @@ export interface EditableProps { oneLine?: boolean; editing?: boolean; onClick?: (e: React.MouseEvent) => boolean; + isEditingCallback?: (isEditing: boolean) => void; } /** @@ -48,6 +49,11 @@ export class EditableView extends React.Component<EditableProps> { } @action + componentWillReceiveProps(nextProps: EditableProps) { + this._editing = nextProps.editing ? true : false; + } + + @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Tab") { this.props.OnTab && this.props.OnTab(); @@ -55,13 +61,16 @@ export class EditableView extends React.Component<EditableProps> { if (!e.ctrlKey) { if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) { this._editing = false; + this.props.isEditingCallback && this.props.isEditingCallback(false); } } else if (this.props.OnFillDown) { this.props.OnFillDown(e.currentTarget.value); this._editing = false; + this.props.isEditingCallback && this.props.isEditingCallback(false); } } else if (e.key === "Escape") { this._editing = false; + this.props.isEditingCallback && this.props.isEditingCallback(false); } } @@ -69,6 +78,7 @@ export class EditableView extends React.Component<EditableProps> { onClick = (e: React.MouseEvent) => { if (!this.props.onClick || !this.props.onClick(e)) { this._editing = true; + this.props.isEditingCallback && this.props.isEditingCallback(true); } e.stopPropagation(); } @@ -85,7 +95,7 @@ export class EditableView extends React.Component<EditableProps> { render() { if (this._editing) { return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus - onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} + onBlur={action(() => {this._editing = false; this.props.isEditingCallback && this.props.isEditingCallback(false);})} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display, fontSize: this.props.fontSize }} />; } else { return ( diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 72d993b5a..ab027c425 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -134,7 +134,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { }; let onItemDown = (e: React.PointerEvent) => { SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc), this._document[props.fieldKey] instanceof Doc ? "alias" : undefined)(e); + this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); }; let field = props.Document[props.fieldKey]; @@ -153,7 +153,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { <div className="collectionSchemaView-cellContents" ref={this.dropRef} onPointerDown={onItemDown} key={props.Document[Id]}> <EditableView editing={this._isEditing} - // isEditingCallback={this.isEditingCallback} + isEditingCallback={this.isEditingCallback} display={"inline"} contents={contents} height={Number(MAX_ROW_HEIGHT)} @@ -229,8 +229,8 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { render() { let reference = React.createRef<HTMLDivElement>(); let onItemDown = (e: React.PointerEvent) => { - // (!this.props.CollectionView.props.isSelected() ? undefined : - // SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); + (!this.props.CollectionView.props.isSelected() ? undefined : + SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); }; return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index d6ebaf8d8..316b3f0ff 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Flyout, anchorPoints } from "../DocumentDecorations"; import { ColumnType } from "./CollectionSchemaView"; import { emptyFunction } from "../../../Utils"; +import { contains } from "typescript-collections/dist/lib/arrays"; library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn); @@ -21,6 +22,8 @@ export interface HeaderProps { setIsEditing: (isEditing: boolean) => void; deleteColumn: (column: string) => void; setColumnType: (key: string, type: ColumnType) => void; + setColumnSort: (key: string, desc: boolean) => void; + removeColumnSort: (key: string) => void; } export class CollectionSchemaHeader extends React.Component<HeaderProps> { @@ -43,6 +46,8 @@ export class CollectionSchemaHeader extends React.Component<HeaderProps> { deleteColumn={this.props.deleteColumn} onlyShowOptions={false} setColumnType={this.props.setColumnType} + setColumnSort={this.props.setColumnSort} + removeColumnSort={this.props.removeColumnSort} /> </div> ); @@ -59,18 +64,10 @@ export interface AddColumnHeaderProps { @observer export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> { - // @observable private _creatingColumn: boolean = false; - - // @action - // onClick = (e: React.MouseEvent): void => { - // this._creatingColumn = true; - // } - render() { let addButton = <button><FontAwesomeIcon icon="plus" size="sm" /></button>; return ( <div className="collectionSchemaView-header-addColumn" > - {/* {this._creatingColumn ? <></> : */} <CollectionSchemaColumnMenu keyValue="" possibleKeys={this.props.possibleKeys} @@ -84,6 +81,8 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe deleteColumn={action(emptyFunction)} onlyShowOptions={true} setColumnType={action(emptyFunction)} + setColumnSort={action(emptyFunction)} + removeColumnSort={action(emptyFunction)} /> </div> ); @@ -105,12 +104,36 @@ export interface ColumnMenuProps { deleteColumn: (column: string) => void; onlyShowOptions: boolean; setColumnType: (key: string, type: ColumnType) => void; + setColumnSort: (key: string, desc: boolean) => void; + removeColumnSort: (key: string) => void; } @observer export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> { @observable private _isOpen: boolean = false; + @observable private _node : HTMLDivElement | null = null; + // @observable private _node = React.createRef<HTMLDivElement>(); + @observable private _test = "test"; + + componentDidMount() { + document.addEventListener("pointerdown", this.detectClick); + console.log("did mount", this._node); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.detectClick); + } - @action toggleIsOpen = (): void => { + detectClick = (e: PointerEvent): void => { + console.log("click", this); + if (this._node && this._node.contains(e.target as Node)) { + } else { + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + @action + toggleIsOpen = (): void => { this._isOpen = !this._isOpen; this.props.setIsEditing(this._isOpen); } @@ -121,6 +144,14 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> this.props.setColumnType(this.props.keyValue, type); } + @action + setNode = (node: HTMLDivElement): void => { + if (node) { + this._node = node; + console.log("set node to ", this._node); + } + } + renderTypes = () => { if (this.props.typeConst) return <></>; return ( @@ -147,6 +178,19 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> ); } + renderSorting = () => { + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Sort by:</label> + <div className="columnMenu-sort"> + <div className="columnMenu-option" onClick={() => this.props.setColumnSort(this.props.keyValue, false)}>Sort ascending</div> + <div className="columnMenu-option" onClick={() => this.props.setColumnSort(this.props.keyValue, true)}>Sort descending</div> + <div className="columnMenu-option" onClick={() => this.props.removeColumnSort(this.props.keyValue)}>Clear sorting</div> + </div> + </div> + ); + } + renderContent = () => { return ( <div className="collectionSchema-header-menuOptions"> @@ -159,11 +203,13 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> canAddNew={true} addNew={this.props.addNew} onSelect={this.props.onSelect} + setIsEditing={this.props.setIsEditing} /> </div> {this.props.onlyShowOptions ? <></> : <> {this.renderTypes()} + {this.renderSorting()} <div className="collectionSchema-headerMenu-group"> <button onClick={() => this.props.deleteColumn(this.props.keyValue)}>Delete Column</button> </div> @@ -174,21 +220,17 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } render() { + console.log("render", this._node); return ( - <div className="collectionSchema-header-menu"> + <div className="collectionSchema-header-menu" ref={this.setNode}> <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderContent()}> - <div className="collectionSchema-header-toggler" onClick={() => { this.props.setIsEditing(true); }}>{this.props.menuButtonContent}</div> + <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div> </ Flyout > </div> ); } } -{/* // <div className="collectionSchema-header-menu"> - // <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div> - // {this.renderContent()} - // </div> */} - interface KeysDropdownProps { keyValue: string; @@ -197,6 +239,7 @@ interface KeysDropdownProps { canAddNew: boolean; addNew: boolean; onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + setIsEditing: (isEditing: boolean) => void; } @observer class KeysDropdown extends React.Component<KeysDropdownProps> { @@ -214,6 +257,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.onSelect(this._key, key, this.props.addNew); this.setKey(key); this._isOpen = false; + this.props.setIsEditing(false); } onChange = (val: string): void => { @@ -223,15 +267,15 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { @action onFocus = (e: React.FocusEvent): void => { this._isOpen = true; + this.props.setIsEditing(true); } @action onBlur = (e: React.FocusEvent): void => { - // const that = this; - if (this._canClose) this._isOpen = false; - // setTimeout(function() { // TODO: this might be too hacky lol - // that.setIsOpen(false); - // }, 100); + if (this._canClose) { + this._isOpen = false; + this.props.setIsEditing(false); + } } @action diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx new file mode 100644 index 000000000..3a61881a7 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -0,0 +1,193 @@ +import React = require("react"); +import { ReactTableDefaults, TableCellRenderer, ComponentPropsGetterR, ComponentPropsGetter0 } from "react-table"; +import "./CollectionSchemaView.scss"; +import { Transform } from "../../util/Transform"; +import { Doc } from "../../../new_fields/Doc"; +import { DragManager, SetupDrag } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Cast, FieldValue } from "../../../new_fields/Types"; + + +export interface MovableColumnProps { + columnRenderer: TableCellRenderer; + columnValue: string; + allColumns: string[]; + reorderColumns: (toMove: string, relativeTo: string, before: boolean, columns: string[]) => void; + ScreenToLocalTransform: () => Transform; +} +export class MovableColumn extends React.Component<MovableColumnProps> { + private _header?: React.RefObject<HTMLDivElement> = React.createRef(); + private _colDropDisposer?: DragManager.DragDropDisposer; + + onPointerEnter = (e: React.PointerEvent): void => { + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { + this._header!.current!.className = "collectionSchema-col-wrapper"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + this._header!.current!.className = "collectionSchema-col-wrapper"; + document.removeEventListener("pointermove", this.onDragMove, true); + } + onDragMove = (e: PointerEvent): void => { + let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + let rect = this._header!.current!.getBoundingClientRect(); + let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); + let before = x[0] < bounds[0]; + this._header!.current!.className = "collectionSchema-col-wrapper"; + if (before) this._header!.current!.className += " col-before"; + if (!before) this._header!.current!.className += " col-after"; + e.stopPropagation(); + } + + createColDropTarget = (ele: HTMLDivElement) => { + this._colDropDisposer && this._colDropDisposer(); + if (ele) { + this._colDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.colDrop.bind(this) } }); + } + } + + colDrop = (e: Event, de: DragManager.DropEvent) => { + document.removeEventListener("pointermove", this.onDragMove, true); + let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + let rect = this._header!.current!.getBoundingClientRect(); + let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); + let before = x[0] < bounds[0]; + if (de.data instanceof DragManager.ColumnDragData) { + this.props.reorderColumns(de.data.colKey, this.props.columnValue, before, this.props.allColumns); + return true; + } + return false; + } + + setupDrag (ref: React.RefObject<HTMLElement>) { + let onRowMove = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + let dragData = new DragManager.ColumnDragData(this.props.columnValue); + DragManager.StartColumnDrag(ref.current!, dragData, e.x, e.y); + }; + let onRowUp = (): void => { + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + }; + let onItemDown = (e: React.PointerEvent) => { + if (e.button === 0) { + e.stopPropagation(); + document.addEventListener("pointermove", onRowMove); + document.addEventListener("pointerup", onRowUp); + } + }; + return onItemDown; + } + + + render() { + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = this.setupDrag(reference); + + return ( + <div className="collectionSchema-col" ref={this.createColDropTarget}> + <div className="collectionSchema-col-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <div className="col-dragger" ref={reference} onPointerDown={onItemDown}> + {this.props.columnRenderer} + </div> + </div> + </div> + ); + } +} + +export interface MovableRowProps { + ScreenToLocalTransform: () => Transform; + addDoc: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; + moveDoc: DragManager.MoveFunction; +} + +export class MovableRow extends React.Component<MovableRowProps> { + private _header?: React.RefObject<HTMLDivElement> = React.createRef(); + private _rowDropDisposer?: DragManager.DragDropDisposer; + + onPointerEnter = (e: React.PointerEvent): void => { + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { + this._header!.current!.className = "collectionSchema-row-wrapper"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + this._header!.current!.className = "collectionSchema-row-wrapper"; + document.removeEventListener("pointermove", this.onDragMove, true); + } + onDragMove = (e: PointerEvent): void => { + let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + let rect = this._header!.current!.getBoundingClientRect(); + let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + let before = x[1] < bounds[1]; + this._header!.current!.className = "collectionSchema-row-wrapper"; + if (before) this._header!.current!.className += " row-above"; + if (!before) this._header!.current!.className += " row-below"; + e.stopPropagation(); + } + + createRowDropTarget = (ele: HTMLDivElement) => { + this._rowDropDisposer && this._rowDropDisposer(); + if (ele) { + this._rowDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } }); + } + } + + rowDrop = (e: Event, de: DragManager.DropEvent) => { + const { children = null, rowInfo } = this.props; + if (!rowInfo) return false; + + const { original } = rowInfo; + const rowDoc = FieldValue(Cast(original, Doc)); + if (!rowDoc) return false; + + let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + let rect = this._header!.current!.getBoundingClientRect(); + let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + let before = x[1] < bounds[1]; + if (de.data instanceof DragManager.DocumentDragData) { + e.stopPropagation(); + if (de.data.draggedDocuments[0] === rowDoc) return true; + let addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before); + let movedDocs = de.data.draggedDocuments; //(de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments); + return (de.data.dropAction || de.data.userDropAction) ? + de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, rowDoc, addDocument) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false); + } + return false; + } + + render() { + const { children = null, rowInfo } = this.props; + if (!rowInfo) { + return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>; + } + + const { original } = rowInfo; + const doc = FieldValue(Cast(original, Doc)); + if (!doc) return <></>; + + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = SetupDrag(reference, () => doc, this.props.moveDoc); + + return ( + <div className="collectionSchema-row" ref={this.createRowDropTarget}> + <div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <div className="row-dragger" ref={reference} onPointerDown={onItemDown}> + <ReactTableDefaults.TrComponent> + {children} + </ReactTableDefaults.TrComponent> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 4bc7a778c..4ae9628a9 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -105,6 +105,14 @@ direction: ltr; max-height: $MAX_ROW_HEIGHT; + &:nth-child(even) { + background-color: $light-color; + } + + &:nth-child(odd) { + background-color: $light-color-secondary; + } + &:last-child { border-bottom: $intermediate-color; border-bottom-style: solid; @@ -112,6 +120,10 @@ } } + .rt-tr { + width: 100%; + } + .rt-td { border-width: 1px; border-right-color: $intermediate-color; @@ -149,6 +161,20 @@ background: $light-color; } +.collectionSchema-col{ + height: 100%; + + .collectionSchema-col-wrapper { + &.col-before { + border-left: 2px solid red; + } + &.col-after { + border-right: 2px solid red; + } + } +} + + .collectionSchemaView-header { height: 100%; @@ -168,16 +194,8 @@ } .collectionSchema-header-menuOptions { - // position: absolute; - // top: 30px; - // left: 50%; - // transform: translateX(-50%); - // z-index: 9999; - // background-color: $light-color-secondary; color: black; - // border: 1px solid $main-accent; width: 175px; - // padding: 10px; text-align: left; .collectionSchema-headerMenu-group { @@ -235,15 +253,51 @@ } } -#preview-schema-checkbox-div { - margin-left: 20px; - font-size: 12px; +.collectionSchema-row { + height: $MAX_ROW_HEIGHT; + // display: flex; + + .row-dragger { + height: $MAX_ROW_HEIGHT; + } + + .collectionSchema-row-wrapper { + max-height: $MAX_ROW_HEIGHT; + // width: 100%; + // border: 1px solid lightgray; + + &.row-above { + border-top: 1px solid red; + } + &.row-below { + border-bottom: 1px solid red; + } + &.row-inside { + border: 1px solid red; + } + } } -.-even { - background: $light-color !important; + +.collectionSchemaView-cellWrapper { + // height: $MAX_ROW_HEIGHT; + // background-color: red; + height: 100%; + padding: 4px; + + &.focused { + // background-color: yellowgreen; + border: 2px solid yellowgreen; + + input { + outline: 0; + border: none; + background-color: yellow; + } + } } -.-odd { - background: $light-color-secondary !important; +#preview-schema-checkbox-div { + margin-left: 20px; + font-size: 12px; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 3ef58bcaf..ffc9d7d09 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -4,7 +4,7 @@ import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; -import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, TableCellRenderer } from "react-table"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, TableCellRenderer, Column } from "react-table"; import "react-table/react-table.css"; import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; import { Doc, DocListCast, DocListCastAsync, Field, FieldResult } from "../../../new_fields/Doc"; @@ -33,7 +33,7 @@ import { undoBatch } from "../../util/UndoManager"; import { timesSeries } from "async"; import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders"; import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell } from "./CollectionSchemaCells"; - +import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; library.add(faCog); library.add(faPlus); @@ -54,89 +54,80 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["libraryBrush", ColumnType.Boolean], ["zIndex", ColumnType.Number] ]); -// @observer -// class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> { -// constructor(props: any) { -// super(props); -// } - -// render() { -// return ( -// <div key={this.props.keyName}> -// <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} /> -// {this.props.keyName} -// </div> -// ); -// } -// } - @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { private _mainCont?: HTMLDivElement; private _startPreviewWidth = 0; private DIVIDER_WIDTH = 4; - @observable _columns: Array<string> = ["title", "data", "author"]; @observable _selectedIndex = 0; @observable _columnsPercentage = 0; @observable _keys: string[] = []; @observable _newKeyName: string = ""; @observable previewScript: string = ""; @observable _headerIsEditing: boolean = false; + @observable _cellIsEditing: boolean = false; + @observable _focusedCell: {row: number, col: number} = {row: 0, col: 0}; + @observable _sortedColumns: Map<string, {id: string, desc: boolean}> = new Map(); @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); } - set columns(columns: string[]) { this.props.Document.schemaColumns = new List<string>(columns); } + set columns(columns: string[]) {this.props.Document.schemaColumns = new List<string>(columns); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableColumns() { + @computed get tableColumns(): Column<Doc>[] { let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.toUpperCase() === key.toUpperCase()) === -1); - let cols = this.columns.map(col => { + let focusedRow = this._focusedCell.row; + let focusedCol = this._focusedCell.col; + let isEditable = !this._headerIsEditing; + let header = <CollectionSchemaHeader + keyValue={col} + possibleKeys={possibleKeys} + existingKeys={this.columns} + keyType={this.getColumnType(col)} + typeConst={columnTypes.get(col) !== undefined} + onSelect={this.changeColumns} + setIsEditing={this.setHeaderIsEditing} + deleteColumn={this.deleteColumn} + setColumnType={this.setColumnType} + setColumnSort={this.setColumnSort} + removeColumnSort={this.removeColumnSort} + />; + return { - Header: <CollectionSchemaHeader - keyValue={col} - possibleKeys={possibleKeys} - existingKeys={this.columns} - keyType={this.getColumnType(col)} - typeConst={columnTypes.get(col) !== undefined} - onSelect={this.changeColumns} - setIsEditing={this.setHeaderIsEditing} - deleteColumn={this.deleteColumn} - setColumnType={this.setColumnType} - />, + Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.columns} reorderColumns={this.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, accessor: (doc: Doc) => doc ? doc[col] : 0, id: col, Cell: (rowProps: CellInfo) => { let row = rowProps.index; let column = this.columns.indexOf(rowProps.column.id!); - // let isFocused = focusedRow === row && focusedCol === column; - let isFocused = false; + let isFocused = focusedRow === row && focusedCol === column; let props: CellProps = { row: row, col: column, rowProps: rowProps, isFocused: isFocused, - changeFocusedCellByDirection: action(emptyFunction),//this.changeFocusedCellByDirection, - changeFocusedCellByIndex: action(emptyFunction), //this.changeFocusedCellByIndex, + changeFocusedCellByDirection: this.changeFocusedCellByDirection, + changeFocusedCellByIndex: this.changeFocusedCellByIndex, CollectionView: this.props.CollectionView, ContainingCollection: this.props.ContainingCollectionView, Document: this.props.Document, fieldKey: this.props.fieldKey, renderDepth: this.props.renderDepth, addDocTab: this.props.addDocTab, moveDocument: this.props.moveDocument, - setIsEditing: action(emptyFunction), //this.setCellIsEditing, - isEditable: true //isEditable + setIsEditing: this.setCellIsEditing, + isEditable: isEditable }; let colType = this.getColumnType(col); - if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props}/> - if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props}/> - if (colType === ColumnType.Boolean) return <CollectionSchemaBooleanCell {...props} /> - if (colType === ColumnType.Checkbox) return <CollectionSchemaCheckboxCell {...props} /> - return <CollectionSchemaCell {...props}/> + if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props}/>; + if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props}/>; + if (colType === ColumnType.Boolean) return <CollectionSchemaBooleanCell {...props} />; + if (colType === ColumnType.Checkbox) return <CollectionSchemaCheckboxCell {...props} />; + return <CollectionSchemaCell {...props}/>; } }; }) as {Header: TableCellRenderer, accessor: (doc: Doc) => FieldResult<Field>, id: string, Cell: (rowProps: CellInfo) => JSX.Element}[]; @@ -156,18 +147,26 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return cols; } - onHeaderDrag = (columnName: string) => { - let schemaDoc = Cast(this.props.Document.schemaDoc, Doc); - if (schemaDoc instanceof Doc) { - let columnDocs = DocListCast(schemaDoc.data); - if (columnDocs) { - let ddoc = columnDocs.find(doc => doc.title === columnName); - if (ddoc) { - return ddoc; - } - } - } - return this.props.Document; + // onHeaderDrag = (columnName: string) => { + // let schemaDoc = Cast(this.props.Document.schemaDoc, Doc); + // if (schemaDoc instanceof Doc) { + // let columnDocs = DocListCast(schemaDoc.data); + // if (columnDocs) { + // let ddoc = columnDocs.find(doc => doc.title === columnName); + // if (ddoc) { + // return ddoc; + // } + // } + // } + // return this.props.Document; + // } + + componentDidMount() { + document.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); } private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { @@ -176,6 +175,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return {}; } return { + ScreenToLocalTransform: this.props.ScreenToLocalTransform, + addDoc: (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before), + moveDoc: (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc), + rowInfo, onClick: action((e: React.MouseEvent, handleOriginal: Function) => { that.props.select(e.ctrlKey); that._selectedIndex = rowInfo.index; @@ -184,10 +187,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { handleOriginal(); } }), - style: { - background: rowInfo.index === this._selectedIndex ? "lightGray" : "white", - //color: rowInfo.index === this._selectedIndex ? "white" : "black" - } }; } @@ -197,23 +196,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @action - setHeaderIsEditing = (isEditing: boolean) => { - this._headerIsEditing = isEditing; + setCellIsEditing = (isEditing: boolean): void => { + this._cellIsEditing = isEditing; } @action - toggleKey = (key: string) => { - let list = Cast(this.props.Document.schemaColumns, listSpec("string")); - if (list === undefined) { - this.props.Document.schemaColumns = list = new List<string>([key]); - } else { - const index = list.indexOf(key); - if (index === -1) { - list.push(key); - } else { - list.splice(index, 1); - } - } + setHeaderIsEditing = (isEditing: boolean): void => { + this._headerIsEditing = isEditing; } //toggles preview side-panel of schema @@ -262,6 +251,45 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } } + onKeyDown = (e: KeyboardEvent): void => { + if (!this._cellIsEditing && !this._headerIsEditing) { + let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; + this.changeFocusedCellByDirection(direction); + } + } + + @action + changeFocusedCellByDirection = (direction: string): void => { + switch (direction) { + case "tab": + if (this._focusedCell.col + 1 === this.columns.length && this._focusedCell.row + 1 === this.childDocs.length) { + this._focusedCell = { row: 0, col: 0 }; + } else if (this._focusedCell.col + 1 === this.columns.length) { + this._focusedCell = { row: this._focusedCell.row + 1, col: 0 }; + } else { + this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 }; + } + break; + case "right": + this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 === this.columns.length ? this._focusedCell.col : this._focusedCell.col + 1 }; + break; + case "left": + this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col === 0 ? this._focusedCell.col : this._focusedCell.col - 1 }; + break; + case "up": + this._focusedCell = { row: this._focusedCell.row === 0 ? this._focusedCell.row : this._focusedCell.row - 1, col: this._focusedCell.col }; + break; + case "down": + this._focusedCell = { row: this._focusedCell.row + 1 === this.childDocs.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col }; + break; + } + } + + @action + changeFocusedCellByIndex = (row: number, col: number): void => { + this._focusedCell = { row: row, col: col }; + } + @action makeDB = async () => { let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); @@ -333,17 +361,38 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let newTypesDoc = new Doc(); newTypesDoc[key] = type; this.props.Document.schemaColumnTypes = newTypesDoc; - console.log("no typesDoc"); return; } else { typesDoc[key] = type; } } - // @action - // newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - // this._newKeyName = e.currentTarget.value; - // } + @action + setColumns = (columns: string[]) => { + this.columns = columns; + } + + reorderColumns = (toMove: string, relativeTo: string, before: boolean, columnsValues: string[]) => { + let columns = [...columnsValues]; + let oldIndex = columns.indexOf(toMove); + let relIndex = columns.indexOf(relativeTo); + let newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex; + + if (oldIndex === newIndex) return; + + columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]); + this.setColumns(columns); + } + + @action + setColumnSort = (column: string, descending: boolean) => { + this._sortedColumns.set(column, {id: column, desc: descending}); + } + + @action + removeColumnSort = (column: string) => { + this._sortedColumns.delete(column); + } @computed get previewDocument(): Doc | undefined { @@ -370,44 +419,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return Array.from(Object.keys(keys)); } - // get documentKeysCheckList() { - // const docs = DocListCast(this.props.Document[this.props.fieldKey]); - // let keys: { [key: string]: boolean } = {}; - // // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. - // // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be - // // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. - // // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu - // // is displayed (unlikely) it won't show up until something else changes. - // //TODO Types - // untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); - - // this.columns.forEach(key => keys[key] = true); - // return Array.from(Object.keys(keys)).map(item => - // (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />)); - // } - - // get tableOptionsPanel() { - // return !this.props.active() ? (null) : - // (<Flyout - // anchorPoint={anchorPoints.RIGHT_TOP} - // content={<div> - // <div id="schema-options-header"><h5><b>Options</b></h5></div> - // <div id="options-flyout-div"> - // <h6 className="schema-options-subHeader">Preview Window</h6> - // <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div> - // <h6 className="schema-options-subHeader" >Displayed Columns</h6> - // <ul id="schema-col-checklist" > - // {this.documentKeysCheckList} - // </ul> - // <input value={this._newKeyName} onChange={this.newKeyChange} /> - // <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> - // </div> - // </div> - // }> - // <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> - // </Flyout>); - // } - @computed get reactTable() { let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1; @@ -418,9 +429,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { pageSize={this.childDocs.length} showPagination={false} columns={this.tableColumns} - // column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} sortable={false} + TrComponent={MovableRow} + sorted={Array.from(this._sortedColumns.values())} />; } @@ -468,10 +480,20 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.previewScript = script; } + @computed + get schemaToolbar() { + return ( + <div className="collectionSchemaView-toolbar"> + <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div> + </div> + ); + } + render() { return ( <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}> + {this.schemaToolbar} {this.reactTable} {this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} @@ -480,6 +502,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { ); } } + + interface CollectionSchemaPreviewProps { Document?: Doc; DataDocument?: Doc; @@ -554,6 +578,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre } return undefined; } + + render() { let input = this.props.previewScript === undefined ? (null) : <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange} |