diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/DragManager.ts | 15 | ||||
-rw-r--r-- | src/client/views/EditableView.tsx | 17 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaCells.tsx | 261 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaHeaders.tsx | 298 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaMovableTableHOC.tsx | 221 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaView.scss | 594 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaView.tsx | 922 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackingView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/CollectionViewChromes.tsx | 26 | ||||
-rw-r--r-- | src/new_fields/Doc.ts | 9 |
10 files changed, 1790 insertions, 579 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 323908302..0299b1d90 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,6 +1,6 @@ import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; -import { Cast } from "../../new_fields/Types"; +import { Cast, StrCast } from "../../new_fields/Types"; import { URLField } from "../../new_fields/URLField"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; @@ -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 a8cfc4fcb..31e4557be 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -39,6 +39,7 @@ export interface EditableProps { oneLine?: boolean; editing?: boolean; onClick?: (e: React.MouseEvent) => boolean; + isEditingCallback?: (isEditing: boolean) => void; } /** @@ -56,6 +57,16 @@ export class EditableView extends React.Component<EditableProps> { } @action + componentWillReceiveProps(nextProps: EditableProps) { + // this is done because when autosuggest is turned on, the suggestions are passed in as a prop, + // so when the suggestions are passed in, and no editing prop is passed in, it used to set it + // to false. this will no longer do so -syip + if (nextProps.editing && nextProps.editing !== this._editing) { + this._editing = nextProps.editing; + } + } + + @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Tab") { this.props.OnTab && this.props.OnTab(); @@ -63,13 +74,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); } } @@ -78,6 +92,7 @@ export class EditableView extends React.Component<EditableProps> { e.nativeEvent.stopPropagation(); if (!this.props.onClick || !this.props.onClick(e)) { this._editing = true; + this.props.isEditingCallback && this.props.isEditingCallback(true); } e.stopPropagation(); } @@ -109,7 +124,7 @@ export class EditableView extends React.Component<EditableProps> { }} /> : <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 { if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue(); diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx new file mode 100644 index 000000000..4a618a436 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -0,0 +1,261 @@ +import React = require("react"); +import { action, computed, observable, trace, untracked, toJS } from "mobx"; +import { observer } from "mobx-react"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, Column } from "react-table"; +import "react-table/react-table.css"; +import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { SetupDrag, DragManager } from "../../util/DragManager"; +import { CompileScript } from "../../util/Scripting"; +import { Transform } from "../../util/Transform"; +import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.scss'; +import '../DocumentDecorations.scss'; +import { EditableView } from "../EditableView"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { CollectionPDFView } from "./CollectionPDFView"; +import "./CollectionSchemaView.scss"; +import { CollectionVideoView } from "./CollectionVideoView"; +import { CollectionView } from "./CollectionView"; +import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types"; +import { Docs } from "../../documents/Documents"; +import { DocumentContentsView } from "../nodes/DocumentContentsView"; +import { SelectionManager } from "../../util/SelectionManager"; + + +export interface CellProps { + row: number; + col: number; + rowProps: CellInfo; + CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + Document: Doc; + fieldKey: string; + renderDepth: number; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + isFocused: boolean; + changeFocusedCellByIndex: (row: number, col: number) => void; + setIsEditing: (isEditing: boolean) => void; + isEditable: boolean; +} + +@observer +export class CollectionSchemaCell extends React.Component<CellProps> { + @observable protected _isEditing: boolean = false; + protected _focusRef = React.createRef<HTMLDivElement>(); + protected _document = this.props.rowProps.original; + private _dropDisposer?: DragManager.DragDropDisposer; + + componentDidMount() { + document.addEventListener("keydown", this.onKeyDown); + + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); + } + + @action + onKeyDown = (e: KeyboardEvent): void => { + if (this.props.isFocused && this.props.isEditable) { + document.removeEventListener("keydown", this.onKeyDown); + this._isEditing = true; + this.props.setIsEditing(true); + } + } + + @action + isEditingCallback = (isEditing: boolean): void => { + document.addEventListener("keydown", this.onKeyDown); + this._isEditing = isEditing; + this.props.setIsEditing(isEditing); + this.props.changeFocusedCellByIndex(this.props.row, this.props.col); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + this.props.changeFocusedCellByIndex(this.props.row, this.props.col); + } + + applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { + const res = run({ this: doc }); + if (!res.success) return false; + doc[this.props.rowProps.column.id as string] = res.result; + return true; + } + + private drop = (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.DocumentDragData) { + let fieldKey = this.props.rowProps.column.id as string; + if (de.data.draggedDocuments.length === 1) { + this._document[fieldKey] = de.data.draggedDocuments[0]; + } + else { + let coll = Docs.Create.SchemaDocument(["title"], de.data.draggedDocuments, {}); + this._document[fieldKey] = coll; + } + e.stopPropagation(); + } + } + + private dropRef = (ele: HTMLElement) => { + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + + renderCellWithType(type: string | undefined) { + let dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + + let props: FieldViewProps = { + Document: this.props.rowProps.original, + DataDoc: this.props.rowProps.original, + fieldKey: this.props.rowProps.column.id as string, + fieldExt: "", + ContainingCollectionView: this.props.CollectionView, + isSelected: returnFalse, + select: emptyFunction, + renderDepth: this.props.renderDepth + 1, + selectOnLoad: false, + ScreenToLocalTransform: Transform.Identity, + focus: emptyFunction, + active: returnFalse, + whenActiveChanged: emptyFunction, + PanelHeight: returnZero, + PanelWidth: returnZero, + addDocTab: this.props.addDocTab, + }; + + // let onItemDown = (e: React.PointerEvent) => { + // SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, + // 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 onPointerEnter = (e: React.PointerEvent): void => { + if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) { + dragRef!.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; + } + }; + let onPointerLeave = (e: React.PointerEvent): void => { + dragRef!.current!.className = "collectionSchemaView-cellContainer"; + }; + + let field = props.Document[props.fieldKey]; + let contents: any = "incorrect type"; + if (type === undefined) contents = <FieldView {...props} />; + if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--"; + if (type === "string") contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--"; + if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--"; + if (type === "document") { + let doc = FieldValue(Cast(field, Doc)); + contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; + } + + let className = "collectionSchemaView-cellWrapper"; + if (this._isEditing) className += " editing"; + if (this.props.isFocused && this.props.isEditable) className += " focused"; + if (this.props.isFocused && !this.props.isEditable) className += " inactive"; + + return ( + <div className="collectionSchemaView-cellContainer" ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> + <div className={className} ref={this._focusRef} tabIndex={-1}> + <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={"inline"} + contents={contents} + height={Number(MAX_ROW_HEIGHT)} + GetValue={() => { + let field = props.Document[props.fieldKey]; + if (Field.IsField(field)) { + return Field.toScriptString(field); + } + return ""; + } + } + SetValue={(value: string) => { + let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name } }); + if (!script.compiled) { + return false; + } + return this.applyToDoc(props.Document, script.run); + }} + OnFillDown={async (value: string) => { + let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name } }); + if (!script.compiled) { + return; + } + const run = script.run; + //TODO This should be able to be refactored to compile the script once + const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); + val && val.forEach(doc => this.applyToDoc(doc, run)); + }} /> + </div > + </div> + </div> + ); + } + + render() { + return this.renderCellWithType(undefined); + } +} + +@observer +export class CollectionSchemaNumberCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType("number"); + } +} + +@observer +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType("boolean"); + } +} + +@observer +export class CollectionSchemaStringCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType("string"); + } +} + +@observer +export class CollectionSchemaDocCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType("document"); + } +} + +@observer +export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { + @observable private _isChecked: boolean = typeof this.props.rowProps.original[this.props.rowProps.column.id as string] === "boolean" ? BoolCast(this.props.rowProps.original[this.props.rowProps.column.id as string]) : false; + + @action + toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { + this._isChecked = e.target.checked; + let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); + if (script.compiled) { + this.applyToDoc(this._document, script.run); + } + } + + render() { + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = (e: React.PointerEvent) => { + (!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}> + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={this._document[Id]} ref={reference}> + <input type="checkbox" checked={this._isChecked} onChange={this.toggleChecked} /> + </div > + </div> + ); + } +} diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx new file mode 100644 index 000000000..d1d0674c4 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -0,0 +1,298 @@ +import React = require("react"); +import { action, computed, observable, trace, untracked } from "mobx"; +import { observer } from "mobx-react"; +import "./CollectionSchemaView.scss"; +import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn } from '@fortawesome/free-solid-svg-icons'; +import { library, IconProp } from "@fortawesome/fontawesome-svg-core"; +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"; +import { faFile } from "@fortawesome/free-regular-svg-icons"; + +library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile); + +export interface HeaderProps { + keyValue: string; + possibleKeys: string[]; + existingKeys: string[]; + keyType: ColumnType; + typeConst: boolean; + onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + 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> { + render() { + let icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" : + this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : "align-justify"; + + return ( + <div className="collectionSchemaView-header" > + <CollectionSchemaColumnMenu + keyValue={this.props.keyValue} + possibleKeys={this.props.possibleKeys} + existingKeys={this.props.existingKeys} + keyType={this.props.keyType} + typeConst={this.props.typeConst} + menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue}</div>} + addNew={false} + onSelect={this.props.onSelect} + setIsEditing={this.props.setIsEditing} + deleteColumn={this.props.deleteColumn} + onlyShowOptions={false} + setColumnType={this.props.setColumnType} + setColumnSort={this.props.setColumnSort} + removeColumnSort={this.props.removeColumnSort} + /> + </div> + ); + } +} + + +export interface AddColumnHeaderProps { + createColumn: () => void; +} + +@observer +export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> { + render() { + return ( + <button className="add-column" onClick={() => this.props.createColumn()}><FontAwesomeIcon icon="plus" size="sm" /></button> + ); + } +} + + + +export interface ColumnMenuProps { + keyValue: string; + possibleKeys: string[]; + existingKeys: string[]; + keyType: ColumnType; + typeConst: boolean; + menuButtonContent: JSX.Element; + addNew: boolean; + onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + setIsEditing: (isEditing: boolean) => void; + deleteColumn: (column: string) => void; + onlyShowOptions: boolean; + setColumnType: (key: string, type: ColumnType) => void; + setColumnSort: (key: string, desc: boolean) => void; + removeColumnSort: (key: string) => void; + anchorPoint?: any; +} +@observer +export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> { + @observable private _isOpen: boolean = false; + @observable private _node: HTMLDivElement | null = null; + + componentDidMount() { + document.addEventListener("pointerdown", this.detectClick); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.detectClick); + } + + detectClick = (e: PointerEvent): void => { + 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); + } + + setColumnType = (oldKey: string, newKey: string, addnew: boolean) => { + let typeStr = newKey as keyof typeof ColumnType; + let type = ColumnType[typeStr]; + this.props.setColumnType(this.props.keyValue, type); + } + + @action + setNode = (node: HTMLDivElement): void => { + if (node) { + this._node = node; + } + } + + renderTypes = () => { + if (this.props.typeConst) return <></>; + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Column type:</label> + <div className="columnMenu-types"> + <button title="Any" className={this.props.keyType === ColumnType.Any ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Any)}> + <FontAwesomeIcon icon={"align-justify"} size="sm" /> + </button> + <button title="Number" className={this.props.keyType === ColumnType.Number ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Number)}> + <FontAwesomeIcon icon={"hashtag"} size="sm" /> + </button> + <button title="String" className={this.props.keyType === ColumnType.String ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.String)}> + <FontAwesomeIcon icon={"font"} size="sm" /> + </button> + <button title="Checkbox" className={this.props.keyType === ColumnType.Boolean ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Boolean)}> + <FontAwesomeIcon icon={"check-square"} size="sm" /> + </button> + <button title="Document" className={this.props.keyType === ColumnType.Doc ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Doc)}> + <FontAwesomeIcon icon={"file"} size="sm" /> + </button> + </div> + </div> + ); + } + + 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"> + <label>Key:</label> + <div className="collectionSchema-headerMenu-group"> + <KeysDropdown + keyValue={this.props.keyValue} + possibleKeys={this.props.possibleKeys} + existingKeys={this.props.existingKeys} + 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> + </> + } + </div> + ); + } + + render() { + return ( + <div className="collectionSchema-header-menu" ref={this.setNode}> + <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}> + <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div> + </ Flyout > + </div> + ); + } +} + + +interface KeysDropdownProps { + keyValue: string; + possibleKeys: string[]; + existingKeys: string[]; + canAddNew: boolean; + addNew: boolean; + onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + setIsEditing: (isEditing: boolean) => void; +} +@observer +class KeysDropdown extends React.Component<KeysDropdownProps> { + @observable private _key: string = this.props.keyValue; + @observable private _searchTerm: string = ""; + @observable private _isOpen: boolean = false; + @observable private _canClose: boolean = true; + + @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; + @action setKey = (key: string): void => { this._key = key; }; + @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; }; + + @action + onSelect = (key: string): void => { + this.props.onSelect(this._key, key, this.props.addNew); + this.setKey(key); + this._isOpen = false; + this.props.setIsEditing(false); + } + + onChange = (val: string): void => { + this.setSearchTerm(val); + } + + @action + onFocus = (e: React.FocusEvent): void => { + this._isOpen = true; + this.props.setIsEditing(true); + } + + @action + onBlur = (e: React.FocusEvent): void => { + if (this._canClose) { + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + @action + onPointerEnter = (e: React.PointerEvent): void => { + this._canClose = false; + } + + @action + onPointerOut = (e: React.PointerEvent): void => { + this._canClose = true; + } + + renderOptions = (): JSX.Element[] | JSX.Element => { + if (!this._isOpen) return <></>; + + let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || + this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; + + let options = keyOptions.map(key => { + return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + }); + + // if search term does not already exist as a group type, give option to create new group type + if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + options.push(<div key={""} className="key-option" + onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> + Create "{this._searchTerm}" key</div>); + } + + return options; + } + + render() { + return ( + <div className="keys-dropdown"> + <input className="keys-search" type="text" value={this._searchTerm} placeholder="Search for or create a new key" + onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input> + <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}> + {this.renderOptions()} + </div> + </div > + ); + } +} diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx new file mode 100644 index 000000000..f45575143 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -0,0 +1,221 @@ +import React = require("react"); +import { ReactTableDefaults, TableCellRenderer, ComponentPropsGetterR, ComponentPropsGetter0, RowInfo } 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, StrCast } from "../../../new_fields/Types"; +import { ContextMenu } from "../ContextMenu"; +import { action } from "mobx"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +library.add(faGripVertical, faTrash); + +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; + } + + // onColDrag = (e: React.DragEvent, ref: React.RefObject<HTMLDivElement>) => { + // this.setupDrag(reference); + // } + + + 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 { + rowInfo: RowInfo; + ScreenToLocalTransform: () => Transform; + addDoc: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; + removeDoc: (doc: Doc) => boolean; + rowFocused: boolean; + textWrapRow: (doc: Doc) => void; + rowWrapped: boolean; +} + +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 rowDoc = FieldValue(Cast(this.props.rowInfo.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; + 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; + } + + onRowContextMenu = (e: React.MouseEvent): void => { + let description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; + ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original) }); + } + + @action + move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { + return doc !== target && this.props.removeDoc(doc) && addDoc(doc); + } + + 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.move); + + let className = "collectionSchema-row"; + if (this.props.rowFocused) className += " row-focused"; + if (this.props.rowWrapped) className += " row-wrapped"; + // if (!this.props.rowWrapped) className += " row-unwrapped"; + + return ( + <div className={className} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> + <div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <ReactTableDefaults.TrComponent> + <div className="row-dragger"> + <div className="row-option" onClick={() => this.props.removeDoc(this.props.rowInfo.original)}><FontAwesomeIcon icon="trash" size="sm" /></div> + <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> + </div> + {children} + </ReactTableDefaults.TrComponent> + </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 1e6d1fe99..91c6e8b3c 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,27 +1,28 @@ @import "../globalCssVariables"; - - .collectionSchemaView-container { border-width: $COLLECTION_BORDER_WIDTH; border-color: $intermediate-color; border-style: solid; border-radius: $border-radius; box-sizing: border-box; - position: absolute; + // position: absolute; width: 100%; - height: 100%; + height: calc(100% - 50px); + // overflow: hidden; + // overflow-x: scroll; + // border: none; overflow: hidden; transition: top 0.5s; - .collectionSchemaView-cellContents { - height: $MAX_ROW_HEIGHT; + // .collectionSchemaView-cellContents { + // height: $MAX_ROW_HEIGHT; - img { - width: auto; - max-height: $MAX_ROW_HEIGHT; - } - } + // img { + // width: auto; + // max-height: $MAX_ROW_HEIGHT; + // } + // } .collectionSchemaView-previewRegion { position: relative; @@ -48,329 +49,418 @@ } } - .collectionSchemaView-previewHandle { - position: absolute; - height: 15px; - width: 15px; - z-index: 20; - right: 0; - top: 20px; - background: Black; - } - .collectionSchemaView-dividerDragger { position: relative; - background: black; float: left; - height: 37px; + height: 100%; width: 20px; z-index: 20; right: 0; top: 0; - background: $main-accent; - } - - .collectionSchemaView-columnsHandle { - position: absolute; - height: 37px; - width: 20px; - z-index: 20; - left: 0; - bottom: 0; - background: $main-accent; + background: gray; + cursor: col-resize; + // background: $main-accent; + // box-sizing: border-box; + // border-left: 1px solid $intermediate-color; + // border-right: 1px solid $intermediate-color; } +} - .collectionSchemaView-colDividerDragger { - position: relative; - box-sizing: border-box; - border-top: 1px solid $intermediate-color; - border-bottom: 1px solid $intermediate-color; - float: top; - width: 100%; - } +.ReactTable { + width: 100%; + height: 100%; + background: white; + box-sizing: border-box; + border: none !important; - .collectionSchemaView-dividerDragger { - position: relative; - box-sizing: border-box; - border-left: 1px solid $intermediate-color; - border-right: 1px solid $intermediate-color; - float: left; + .rt-table { + overflow-y: auto; + overflow-x: auto; height: 100%; + display: -webkit-inline-box; + direction: ltr; } - .collectionSchemaView-tableContainer { - position: relative; - float: left; - height: 100%; - } + .rt-thead { + width: calc(100% - 50px); + margin-left: 50px; + + &.-header { + // background: $intermediate-color; + // color: $light-color; + font-size: 12px; + height: 30px; + // border: 1px solid $intermediate-color; + box-shadow: none; + // width: calc(100% - 30px); + // margin-right: -30px; + } - .ReactTable { - // position: absolute; // display: inline-block; - // overflow: auto; - width: 100%; - height: 100%; - background: $light-color; - box-sizing: border-box; - border: none !important; + .rt-resizable-header { + padding: 0; + height: 30px; - .rt-table { - overflow-y: auto; - overflow-x: auto; - height: 100%; - display: -webkit-inline-box; - direction: ltr; // direction:rtl; - // display:block; + &:last-child { + overflow: visible; + } } - .rt-tbody { - //direction: ltr; - direction: rtl; + .rt-resizable-header-content { + height: 100%; + overflow: visible; } - .rt-tr-group { - direction: ltr; - max-height: $MAX_ROW_HEIGHT; + .rt-th { + padding: 0; + border: solid lightgray; + border-width: 0 1px; } + } - .rt-td { - border-width: 1px; - border-right-color: $intermediate-color; - - .imageBox-cont { - position: relative; - max-height: 100%; - } + .rt-th { + // max-height: $MAX_ROW_HEIGHT; + font-size: 13px; + text-align: center; + background-color: $light-color-secondary; + + &:last-child { + overflow: visible; + } + } - .imageBox-cont img { - object-fit: contain; - max-width: 100%; - height: 100%; - } + .rt-tbody { + direction: rtl; + overflow: visible; + } - .videoBox-cont { - object-fit: contain; - width: auto; - height: 100%; - } - } + .rt-tr-group { + direction: ltr; + flex: 0 1 auto; + min-height: 30px; + border: 0 !important; + // border: solid lightgray; + // border-width: 1px 0; + // border-left: 1px solid lightgray; + // max-height: $MAX_ROW_HEIGHT; + // for sub comp + + // &:nth-child(even) { + // background-color: $light-color; + // } + + // &:nth-child(odd) { + // background-color: $light-color-secondary; + // } + + // &:first-child { + // border-top: 1px solid $light-color-secondary !important; + // } + // &:last-child { + // border-bottom: 1px solid $light-color-secondary !important; + // } } - .ReactTable .rt-thead.-header { - background: $intermediate-color; - color: $light-color; - // text-transform: uppercase; - letter-spacing: 2px; - font-size: 12px; - height: 30px; - padding-top: 4px; + .rt-tr { + width: 100%; + min-height: 30px; + // height: $MAX_ROW_HEIGHT; } - .ReactTable .rt-th, - .ReactTable .rt-td { - max-height: $MAX_ROW_HEIGHT; - padding: 3px 7px; + .rt-td { + // border: 1px solid $light-color-secondary !important; + // border-width: 0 1px; + // border-width: 1px; + // border-right-color: $intermediate-color; + // max-height: $MAX_ROW_HEIGHT; + padding: 0; font-size: 13px; text-align: center; - } + + // white-space: normal; - .ReactTable .rt-tbody .rt-tr-group:last-child { - border-bottom: $intermediate-color; - border-bottom-style: solid; - border-bottom-width: 1; - } + .imageBox-cont { + position: relative; + max-height: 100%; + } - .documentView-node-topmost { - text-align: left; - transform-origin: center top; - display: inline-block; - } + .imageBox-cont img { + object-fit: contain; + max-width: 100%; + height: 100%; + } - .documentView-node:first-child { - background: $light-color; + .videoBox-cont { + object-fit: contain; + width: auto; + height: 100%; + } } } -//options menu styling -#schemaOptionsMenuBtn { - position: absolute; - height: 20px; - width: 20px; - border-radius: 50%; - z-index: 21; - right: 4px; - top: 4px; - pointer-events: auto; - background-color: black; +.documentView-node-topmost { + text-align: left; + transform-origin: center top; display: inline-block; - padding: 0px; - font-size: 100%; } -ul { - list-style-type: disc; +.documentView-node:first-child { + background: $light-color; } -#schema-options-header { - text-align: center; - padding: 0px; - margin: 0px; -} +.collectionSchema-col{ + height: 100%; -.schema-options-subHeader { - color: $intermediate-color; - margin-bottom: 5px; -} + .collectionSchema-col-wrapper { + &.col-before { + border-left: 2px solid red; + } + &.col-after { + border-right: 2px solid red; + } + } +} -#schemaOptionsMenuBtn:hover { - transform: scale(1.15); -} -#preview-schema-checkbox-div { - margin-left: 20px; - font-size: 12px; +.collectionSchemaView-header { + height: 100%; + color: gray; + + .collectionSchema-header-menu { + height: 100%; + + .collectionSchema-header-toggler { + width: 100%; + height: 100%; + padding: 4px; + letter-spacing: 2px; + text-transform: uppercase; + + svg { + margin-right: 4px; + } + } + + // div[class*="css"] { + // width: 100%; + // height: 100%; + // } + } } -#options-flyout-div { - text-align: left; - padding: 0px; - z-index: 100; - font-family: $sans-serif; - padding-left: 5px; +button.add-column { + width: 28px; } -#schema-col-checklist { - overflow: scroll; +.collectionSchema-header-menuOptions { + color: black; + width: 175px; text-align: left; - //background-color: $light-color-secondary; - line-height: 25px; - max-height: 175px; - font-family: $sans-serif; - font-size: 12px; -} + .collectionSchema-headerMenu-group { + margin-bottom: 10px; + } -.Resizer { - box-sizing: border-box; - background: #000; - opacity: 0.5; - z-index: 1; - background-clip: padding-box; - - &.horizontal { - height: 11px; - margin: -5px 0; - border-top: 5px solid rgba(255, 255, 255, 0); - border-bottom: 5px solid rgba(255, 255, 255, 0); - cursor: row-resize; + label { + color: $main-accent; + font-weight: normal; + } + + input { + color: black; width: 100%; + } + + .keys-dropdown { + position: relative; + max-width: 175px; - &:hover { - border-top: 5px solid rgba(0, 0, 0, 0.5); - border-bottom: 5px solid rgba(0, 0, 0, 0.5); + .keys-options-wrapper { + width: 100%; + max-height: 150px; + overflow-y: scroll; + position: absolute; + top: 20px; + + .key-option { + background-color: $light-color; + border: 1px solid $light-color-secondary; + padding: 2px 3px; + + &:not(:last-child) { + border-top: 0; + } + + &:hover { + background-color: $light-color-secondary; + } + } } } - &.vertical { - width: 11px; - margin: 0 -5px; - border-left: 5px solid rgba(255, 255, 255, 0); - border-right: 5px solid rgba(255, 255, 255, 0); - cursor: col-resize; + .columnMenu-types { + display: flex; + justify-content: space-between; - &:hover { - border-left: 5px solid rgba(0, 0, 0, 0.5); - border-right: 5px solid rgba(0, 0, 0, 0.5); + button { + border-radius: 20px; } } +} - &:hover { - -webkit-transition: all 2s ease; - transition: all 2s ease; +.collectionSchema-row { + // height: $MAX_ROW_HEIGHT; + height: 100%; + background-color: white; + + &.row-focused { + background-color: rgb(255, 246, 246);//$light-color-secondary; } -} -.vertical { - section { - width: 100vh; - height: 100vh; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; + &.row-wrapped { + white-space: normal; } - header { - padding: 1rem; - background: #eee; + .row-dragger { + display: flex; + justify-content: space-around; + // height: $MAX_ROW_HEIGHT; + flex: 50 0 auto; + width: 50px; + max-width: 50px; + height: 100%; + min-height: 30px; + // padding: 5px 5px 5px 0; + color: lightgray; + background-color: white; + transition: color 0.1s ease; + + // &:hover { + // color: lightgray; + // } + + .row-option { + // padding: 5px; + cursor: pointer; + transition: color 0.1s ease; + display: flex; + flex-direction: column; + justify-content: center; + + &:hover { + color: gray; + } + } } - footer { - padding: 1rem; - background: #eee; + .collectionSchema-row-wrapper { + // max-height: $MAX_ROW_HEIGHT; + + &.row-above { + border-top: 1px solid red; + } + &.row-below { + border-bottom: 1px solid red; + } + &.row-inside { + border: 1px solid red; + } + + .row-dragging { + background-color: blue; + } } } -.horizontal { - section { - width: 100vh; - height: 100vh; - display: flex; - flex-direction: column; +.collectionSchemaView-cellContainer { + width: 100%; + height: 100%; +} + +.collectionSchemaView-cellWrapper { + height: 100%; + padding: 4px; + + &:focus { + outline: none; } - header { - padding: 1rem; - background: #eee; + &.focused { + // background-color: yellowgreen; + // border: 2px solid yellowgreen; + + input { + outline: 0; + border: none; + background-color: yellow; + } + + &.inactive { + // border: 2px solid rgba(255, 255, 0, 0.4); + border: none; + } } - footer { - padding: 1rem; - background: #eee; + p { + width: 100%; + height: 100%; + // word-wrap: break-word; } } -.parent { - width: 100%; - height: 100%; - -webkit-box-flex: 1; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; +.doc-drag-over { + background-color: red; +} + +.collectionSchemaView-toolbar { + height: 30px; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; + justify-content: flex-end; + padding: 0 10px; + + border-bottom: 2px solid gray; + // margin-bottom: 10px; + + .collectionSchemaView-toolbar-item { + display: flex; + flex-direction: column; + justify-content: center; + } } -.header { - background: #aaa; - height: 3rem; - line-height: 3rem; +#preview-schema-checkbox-div { + margin-left: 20px; + font-size: 12px; } -.wrapper { - background: #ffa; - margin: 5rem; - -webkit-box-flex: 1; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; +.collectionSchemaView-table { + width: calc(100% - 7px); } -.-even { - background: $light-color !important; +.sub { + padding: 10px 30px; + // padding-left: 80px; + background-color: rgb(252, 252, 252); + width: calc(100% - 50px); + margin-left: 50px; + + .rt-table { + overflow-x: hidden; // todo; this shouldnt be like this :(( + overflow-y: visible; + } // TODO fix + + .row-dragger { + background-color: rgb(252, 252, 252); + } + + .rt-table { + background-color: rgb(252, 252, 252); + } + + .collectionSchemaView-table { + width: 100%; + } } -.-odd { - background: $light-color-secondary !important; +.collectionSchemaView-expander { + height: 100%; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index c46698d06..60644b741 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,18 +1,18 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faCog, faPlus, faSortUp, faSortDown } 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 } from "react-table"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, TableCellRenderer, Column, RowInfo } from "react-table"; import "react-table/react-table.css"; import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; -import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Field, FieldResult, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { Docs } from "../../documents/Documents"; +import { Docs, DocumentOptions } from "../../documents/Documents"; import { Gateway } from "../../northstar/manager/Gateway"; import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript, ts, Transformer } from "../../util/Scripting"; @@ -30,29 +30,34 @@ import { CollectionSubView } from "./CollectionSubView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import { undoBatch } from "../../util/UndoManager"; +import { timesSeries } from "async"; +import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders"; +import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells"; +import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; +import { SelectionManager } from "../../util/SelectionManager"; +import { DocumentManager } from "../../util/DocumentManager"; +import { ImageBox } from "../nodes/ImageBox"; import { ComputedField } from "../../../new_fields/ScriptField"; -library.add(faCog); -library.add(faPlus); +library.add(faCog, faPlus, faSortUp, faSortDown); // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 - -@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> - ); - } +export enum ColumnType { + Any, + Number, + String, + Boolean, + Doc, + // Checkbox } +// this map should be used for keys that should have a const type of value +const columnTypes: Map<string, ColumnType> = new Map([ + ["title", ColumnType.String], + ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], + ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], + ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["libraryBrush", ColumnType.Boolean], ["zIndex", ColumnType.Number] +]); @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { @@ -60,183 +65,356 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { 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 previewDoc: Doc | undefined = undefined; + @observable private _node: HTMLDivElement | null = null; + @observable private _focusedTable: Doc = this.props.Document; @computed get chromeCollapsed() { return this.props.chromeCollapsed; } @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"), []); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableColumns() { - return this.columns.map(col => { - const ref = React.createRef<HTMLParagraphElement>(); - return { - Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>, - accessor: (doc: Doc) => doc ? doc[col] : 0, - id: col - }; - }); + + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + super.CreateDropTarget(ele); } - 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; - } - } + // detectClick = (e: PointerEvent): void => { + // if (this._node && this._node.contains(e.target as Node)) { + // } else { + // this._isOpen = false; + // this.props.setIsEditing(false); + // } + // } + + isFocused = (doc: Doc): boolean => { + if (!this.props.isSelected()) return false; + return doc === this._focusedTable; + } + + @action + setFocused = (doc: Doc): void => { + this._focusedTable = doc; + } + + @action + setPreviewDoc = (doc: Doc): void => { + console.log("set"); + this.previewDoc = doc; + } + + //toggles preview side-panel of schema + @action + toggleExpander = () => { + this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; + } + + onDividerDown = (e: React.PointerEvent) => { + this._startPreviewWidth = this.previewWidth(); + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + } + @action + onDividerMove = (e: PointerEvent): void => { + let nativeWidth = this._mainCont!.getBoundingClientRect(); + this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40, + this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]); + } + @action + onDividerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + if (this._startPreviewWidth === this.previewWidth()) { + this.toggleExpander(); } - return this.props.Document; } - getField(row: number, col?: number) { - const docs = DocListCast(this.props.Document[this.props.fieldKey]); - row = row % docs.length; - while (row < 0) row += docs.length; - const columns = this.columns; - const doc = docs[row]; - if (col === undefined) { - return doc; + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (this.props.isSelected()) e.stopPropagation(); } - if (col >= 0 && col < columns.length) { - const column = this.columns[col]; - return doc[column]; + } + + onWheel = (e: React.WheelEvent): void => { + if (this.props.active()) { + e.stopPropagation(); } - return undefined; } - createTransformer = (row: number, col: number): Transformer => { - const self = this; - const captures: { [name: string]: Field } = {}; - - const transformer: ts.TransformerFactory<ts.SourceFile> = context => { - return root => { - function visit(node: ts.Node) { - node = ts.visitEachChild(node, visit, context); - if (ts.isIdentifier(node)) { - const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; - const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; - if (isntPropAccess && isntPropAssign) { - if (node.text === "$r") { - return ts.createNumericLiteral(row.toString()); - } else if (node.text === "$c") { - return ts.createNumericLiteral(col.toString()); - } else if (node.text === "$") { - if (ts.isCallExpression(node.parent)) { - captures.doc = self.props.Document; - captures.key = self.props.fieldKey; - } - } + @computed + get previewDocument(): Doc | undefined { + let selected = this.previewDoc; + let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + console.log("preview document", pdc); + return pdc; + } + + getPreviewTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); + } + + @computed + get dividerDragger() { + return this.previewWidth() === 0 ? (null) : + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; + } + + @computed + get previewPanel() { + let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; + return <div ref={this.createTarget}> + <CollectionSchemaPreview + Document={layoutDoc} + DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} + childDocs={this.childDocs} + renderDepth={this.props.renderDepth} + width={this.previewWidth} + height={this.previewHeight} + getTransform={this.getPreviewTransform} + CollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + setPreviewScript={this.setPreviewScript} + previewScript={this.previewScript} + /> + </div>; + } + @action + setPreviewScript = (script: string) => { + this.previewScript = script; + } + + @computed + get schemaTable() { + return ( + <SchemaTable + Document={this.props.Document} // child doc + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + // childDocs={this.childDocs} + CollectionView={this.props.CollectionView} + ContainingCollectionView={this.props.ContainingCollectionView} + fieldKey={this.props.fieldKey} // might just be this. + renderDepth={this.props.renderDepth} + moveDocument={this.props.moveDocument} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + active={this.props.active} + onDrop={this.onDrop} + addDocTab={this.props.addDocTab} + isSelected={this.props.isSelected} + isFocused={this.isFocused} + setFocused={this.setFocused} + setPreviewDoc={this.setPreviewDoc} + deleteDocument={this.props.removeDocument} + dataDoc={this.props.DataDoc} + /> + ); + } + + @computed + public get schemaToolbar() { + return ( + <div className="collectionSchemaView-toolbar"> + <div className="collectionSchemaView-toolbar-item"> + <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div> + </div> + </div> + ); + } + + render() { + + // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title)); + // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!)) + return ( + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> + {this.schemaTable} + {this.dividerDragger} + {!this.previewWidth() ? (null) : this.previewPanel} + </div> + ); + } +} + +export interface SchemaTableProps { + Document: Doc; // child doc + dataDoc?: Doc; + PanelHeight: () => number; + PanelWidth: () => number; + // childDocs: Doc[]; + CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + fieldKey: string; + renderDepth: number; + deleteDocument: (document: Doc) => boolean; + moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + ScreenToLocalTransform: () => Transform; + // CreateDropTarget: (ele: HTMLDivElement)=> void; // super createdriotarget + active: () => boolean; + onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + isSelected: () => boolean; + isFocused: (document: Doc) => boolean; + setFocused: (document: Doc) => void; + setPreviewDoc: (document: Doc) => void; +} + +@observer +export class SchemaTable extends React.Component<SchemaTableProps> { + // private _mainCont?: HTMLDivElement; + private DIVIDER_WIDTH = 4; + + @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(); + @observable _openCollections: Array<string> = []; + @observable _textWrappedRows: Array<string> = []; + @observable private _node: HTMLDivElement | null = null; + + @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); } + @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } + @computed get tableColumns(): Column<Doc>[] { + let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.toUpperCase() === key.toUpperCase()) === -1); + let columns: Column<Doc>[] = []; + let tableIsFocused = this.props.isFocused(this.props.Document); + let focusedRow = this._focusedCell.row; + let focusedCol = this._focusedCell.col; + let isEditable = !this._headerIsEditing;// && this.props.isSelected(); + + let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + let children = DocListCast(cdoc[this.props.fieldKey]); + + if (children.reduce((found, doc) => found || doc.type === "collection", false)) { + columns.push( + { + expander: true, + Header: "", + width: 30, + Expander: (rowInfo) => { + if (rowInfo.original.type === "collection") { + if (rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-up"} size="sm" /></div>; + if (!rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-down"} size="sm" /></div>; + } else { + return null; } } - - return node; } - return ts.visitNode(root, visit); + ); + } + + let cols = this.columns.map(col => { + + 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: <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 && tableIsFocused; + + let props: CellProps = { + row: row, + col: column, + rowProps: rowProps, + isFocused: isFocused, + 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: 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 <CollectionSchemaCheckboxCell {...props} />; + if (colType === ColumnType.Doc) return <CollectionSchemaDocCell {...props} />; + return <CollectionSchemaCell {...props} />; + }, + minWidth: 200, }; - }; + }); + columns.push(...cols); + + columns.push({ + Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, + accessor: (doc: Doc) => 0, + id: "add", + Cell: (rowProps: CellInfo) => <></>, + width: 28, + resizable: false + }); + return columns; + } - const getVars = () => { - return { capturedVariables: captures }; - }; + // 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; + // } - return { transformer, getVars }; + componentDidMount() { + document.addEventListener("keydown", this.onKeyDown); } - setComputed(script: string, doc: Doc, field: string, row: number, col: number): boolean { - script = - `const $ = (row:number, col?:number) => { - if(col === undefined) { - return (doc as any)[key][row + ${row}]; - } - return (doc as any)[key][row + ${row}][(doc as any).schemaColumns[col + ${col}]]; - } - return ${script}`; - const compiled = CompileScript(script, { params: { this: Doc.name }, typecheck: true, transformer: this.createTransformer(row, col) }); - if (compiled.compiled) { - doc[field] = new ComputedField(compiled); - return true; - } + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); + } - return false; + tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); } - renderCell = (rowProps: CellInfo) => { - let props: FieldViewProps = { - Document: rowProps.original, - DataDoc: rowProps.original, - fieldKey: rowProps.column.id as string, - fieldExt: "", - ContainingCollectionView: this.props.CollectionView, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - selectOnLoad: false, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - addDocTab: this.props.addDocTab, - }; - let fieldContentView = <FieldView {...props} />; - 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)); - }; - let applyToDoc = (doc: Doc, row: number, column: number, run: (args?: { [name: string]: any }) => any) => { - const res = run({ this: doc, $r: row, $c: column, $: (r: number = 0, c: number = 0) => this.getField(r + row, c + column) }); - if (!res.success) return false; - doc[props.fieldKey] = res.result; + tableRemoveDoc = (document: Doc): boolean => { + let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { + children.splice(children.indexOf(document), 1); return true; - }; - const colIndex = this.columns.indexOf(rowProps.column.id!); - return ( - <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> - <EditableView - display={"inline"} - contents={fieldContentView} - height={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - let field = props.Document[props.fieldKey]; - if (Field.IsField(field)) { - return Field.toScriptString(field); - } - return ""; - }} - SetValue={(value: string) => { - if (value.startsWith(":=")) { - return this.setComputed(value.substring(2), props.Document, rowProps.column.id!, rowProps.index, colIndex); - } - let script = CompileScript(value, { addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (!script.compiled) { - return false; - } - return applyToDoc(props.Document, rowProps.index, colIndex, script.run); - }} - OnFillDown={async (value: string) => { - let script = CompileScript(value, { addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (!script.compiled) { - return; - } - const run = script.run; - const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); - val && val.forEach((doc, i) => applyToDoc(doc, i, colIndex, run)); - }}> - </EditableView> - </div > - ); + } + return false; } private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { @@ -245,70 +423,70 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return {}; } return { - onClick: action((e: React.MouseEvent, handleOriginal: Function) => { - that.props.select(e.ctrlKey); - that._selectedIndex = rowInfo.index; + ScreenToLocalTransform: this.props.ScreenToLocalTransform, + addDoc: this.tableAddDoc, + removeDoc: this.tableRemoveDoc, + // removeDoc: this.props.deleteDocument, + rowInfo, + rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document), + textWrapRow: this.textWrapRow, + rowWrapped: this._textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1 + }; + } - if (handleOriginal) { - handleOriginal(); - } - }), + private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => { + if (!rowInfo) return {}; + if (!column) return {}; + + let row = rowInfo.index; + //@ts-ignore + let col = this.columns.indexOf(column!.id); + // let col = column ? this.columns.indexOf(column!) : -1; + let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document); + // let column = this.columns.indexOf(column.id!); + return { style: { - background: rowInfo.index === this._selectedIndex ? "lightGray" : "white", - //color: rowInfo.index === this._selectedIndex ? "white" : "black" + border: !this._headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" } }; } - private createTarget = (ele: HTMLDivElement) => { - this._mainCont = ele; - super.CreateDropTarget(ele); - } + // private createTarget = (ele: HTMLDivElement) => { + // this._mainCont = ele; + // this.props.CreateDropTarget(ele); + // } + + // detectClick = (e: PointerEvent): void => { + // if (this._node && this._node.contains(e.target as Node)) { + // } else { + // this._isOpen = false; + // this.props.setIsEditing(false); + // } + // } @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); - } - } + onExpandCollection = (collection: Doc): void => { + this._openCollections.push(collection[Id]); } - //toggles preview side-panel of schema @action - toggleExpander = () => { - this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; + onCloseCollection = (collection: Doc): void => { + let index = this._openCollections.findIndex(col => col === collection[Id]); + if (index > -1) this._openCollections.splice(index, 1); } - onDividerDown = (e: React.PointerEvent) => { - this._startPreviewWidth = this.previewWidth(); - e.stopPropagation(); - e.preventDefault(); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); - } @action - onDividerMove = (e: PointerEvent): void => { - let nativeWidth = this._mainCont!.getBoundingClientRect(); - this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40, - this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]); + setCellIsEditing = (isEditing: boolean): void => { + this._cellIsEditing = isEditing; } + @action - onDividerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); - if (this._startPreviewWidth === this.previewWidth()) { - this.toggleExpander(); - } + setHeaderIsEditing = (isEditing: boolean): void => { + this._headerIsEditing = isEditing; } onPointerDown = (e: React.PointerEvent): void => { + this.props.setFocused(this.props.Document); if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { if (this.props.isSelected()) e.stopPropagation(); } @@ -320,56 +498,160 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } } - onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB }); + onKeyDown = (e: KeyboardEvent): void => { + if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected()) { + 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 - makeDB = async () => { - let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); - csv = csv.substr(0, csv.length - 1) + "\n"; - let self = this; - DocListCast(this.props.Document.data).map(doc => { - csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", ""); - csv = csv.substr(0, csv.length - 1) + "\n"; - }); - csv.substring(0, csv.length - 1); - let dbName = StrCast(this.props.Document.title); - let res = await Gateway.Instance.PostSchema(csv, dbName); - if (self.props.CollectionView.props.addDocument) { - let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); - if (schemaDoc) { - //self.props.CollectionView.props.addDocument(schemaDoc, false); - self.props.Document.schemaDoc = schemaDoc; + changeFocusedCellByDirection = (direction: string): void => { + let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); + switch (direction) { + case "tab": + if (this._focusedCell.col + 1 === this.columns.length && this._focusedCell.row + 1 === children.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 === children.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col }; + break; + } + const pdoc = FieldValue(children[this._focusedCell.row]); + pdoc && this.props.setPreviewDoc(pdoc); + } + + @action + changeFocusedCellByIndex = (row: number, col: number): void => { + let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); + + this._focusedCell = { row: row, col: col }; + this.props.setFocused(this.props.Document); + + const fdoc = FieldValue(children[this._focusedCell.row]); + fdoc && this.props.setPreviewDoc(fdoc); + } + + createRow = () => { + console.log("creating row"); + let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); + + let newDoc = Docs.Create.TextDocument({ width: 100, height: 30 }); + let proto = Doc.GetProto(newDoc); + proto.title = ""; + children.push(newDoc); + } + + @action + createColumn = () => { + let index = 0; + let found = this.columns.findIndex(col => col.toUpperCase() === "New field".toUpperCase()) > -1; + if (!found) { + this.columns.push("New field"); + return; + } + while (found) { + index++; + found = this.columns.findIndex(col => col.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; + } + this.columns.push("New field (" + index + ")"); + } + + @action + deleteColumn = (key: string) => { + let list = Cast(this.props.Document.schemaColumns, listSpec("string")); + if (list === undefined) { + this.props.Document.schemaColumns = list = new List<string>([]); + } else { + const index = list.indexOf(key); + if (index > -1) { + list.splice(index, 1); } } } @action - addColumn = () => { - this.columns.push(this._newKeyName); - this._newKeyName = ""; + changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { + let list = Cast(this.props.Document.schemaColumns, listSpec("string")); + if (list === undefined) { + this.props.Document.schemaColumns = list = new List<string>([newKey]); + } else { + if (addNew) { + this.columns.push(newKey); + } else { + const index = list.indexOf(oldKey); + if (index > -1) { + list[index] = newKey; + } + } + } + } + + getColumnType = (key: string): ColumnType => { + if (columnTypes.get(key)) return columnTypes.get(key)!; + const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); + if (!typesDoc) return ColumnType.Any; + return NumCast(typesDoc[key]); + } + + setColumnType = (key: string, type: ColumnType): void => { + if (columnTypes.get(key)) return; + const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); + if (!typesDoc) { + let newTypesDoc = new Doc(); + newTypesDoc[key] = type; + this.props.Document.schemaColumnTypes = newTypesDoc; + return; + } else { + typesDoc[key] = type; + } } @action - newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._newKeyName = e.currentTarget.value; + setColumns = (columns: string[]) => { + this.columns = columns; } - @computed - get previewDocument(): Doc | undefined { - const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined; - let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; - return pdc; + 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); } - getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( - - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth) + @action + setColumnSort = (column: string, descending: boolean) => { + this._sortedColumns.set(column, { id: column, desc: descending }); + } + @action + removeColumnSort = (column: string) => { + this._sortedColumns.delete(column); + } - get documentKeysCheckList() { + get documentKeys() { 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. @@ -381,99 +663,99 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { 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>); + return Array.from(Object.keys(keys)); + } + + @action + textWrapRow = (doc: Doc): void => { + let index = this._textWrappedRows.findIndex(id => doc[Id] === id); + if (index > -1) { + this._textWrappedRows.splice(index, 1); + } else { + this._textWrappedRows.push(doc[Id]); + } + } @computed get reactTable() { - let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1; - return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false} + let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + let children = DocListCast(cdoc[this.props.fieldKey]); + + let previewWidth = this.previewWidth(); // + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1; + let hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false); + let expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString()); + let expanded = {}; + //@ts-ignore + expandedRowsList.forEach(row => expanded[row] = true); + console.log(...[...this._textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :(((( + + return <ReactTable + style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} + data={children} + page={0} + pageSize={children.length} + showPagination={false} columns={this.tableColumns} - column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} + getTdProps={this.getTdProps} + sortable={false} + TrComponent={MovableRow} + sorted={Array.from(this._sortedColumns.values())} + expanded={expanded} + SubComponent={hasCollectionChild ? + row => { + if (row.original.type === "collection") { + // let childDocs = DocListCast(row.original[this.props.fieldKey]); + return <div className="sub"><SchemaTable {...this.props} Document={row.original} /></div>; + } + } + : undefined} + />; } - @computed - get dividerDragger() { - return this.previewWidth() === 0 ? (null) : - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB }); + } } - - @computed - get previewPanel() { - // let layoutDoc = this.previewDocument; - // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined; - // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) && - // resolvedDataDoc && resolvedDataDoc !== layoutDoc) { - // // ... so change the layout to be an expanded view of the template layout. This allows the view override the template's properties and be referenceable as its own document. - // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc); - // } - - let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; - return <div ref={this.createTarget}> - <CollectionSchemaPreview - Document={layoutDoc} - DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} - childDocs={this.childDocs} - renderDepth={this.props.renderDepth} - width={this.previewWidth} - height={this.previewHeight} - getTransform={this.getPreviewTransform} - CollectionView={this.props.CollectionView} - moveDocument={this.props.moveDocument} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - active={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - setPreviewScript={this.setPreviewScript} - previewScript={this.previewScript} - /> - </div>; - } @action - setPreviewScript = (script: string) => { - this.previewScript = script; + makeDB = async () => { + let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); + csv = csv.substr(0, csv.length - 1) + "\n"; + let self = this; + DocListCast(this.props.Document.data).map(doc => { + csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", ""); + csv = csv.substr(0, csv.length - 1) + "\n"; + }); + csv.substring(0, csv.length - 1); + let dbName = StrCast(this.props.Document.title); + let res = await Gateway.Instance.PostSchema(csv, dbName); + if (self.props.CollectionView.props.addDocument) { + let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); + if (schemaDoc) { + //self.props.CollectionView.props.addDocument(schemaDoc, false); + self.props.Document.schemaDoc = schemaDoc; + } + } } render() { + // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title)); + // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!)) return ( - <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} - style={{ top: this.chromeCollapsed ? 0 : "unset" }} - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}> + <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={this.onWheel} + onDrop={(e: React.DragEvent) => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {this.dividerDragger} - {!this.previewWidth() ? (null) : this.previewPanel} - {this.tableOptionsPanel} + <button onClick={() => this.createRow()}>new row</button> </div> ); } } + + interface CollectionSchemaPreviewProps { Document?: Doc; DataDocument?: Doc; @@ -552,6 +834,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} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 2d3e585fb..7677f53c1 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -83,7 +83,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { () => { this.props.Document.sectionHeaders = new List(); } - ) + ); } componentWillUnmount() { this._heightDisposer && this._heightDisposer(); @@ -262,7 +262,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { GetValue: () => "", SetValue: this.addGroup, contents: "+ ADD A GROUP" - } + }; // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return ( <div className="collectionStackingView" style={{ top: this.chromeCollapsed ? 0 : 100 }} @@ -272,7 +272,7 @@ 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(this.sortFunc). - map(section => this.section(section[0], section[1] as Doc[])) : + map(section => this.section(section[0], section[1])) : this.section(undefined, this.filteredChildren)} {this.props.Document.sectionFilter ? <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 78ceaad86..1ad9e47ce 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -15,6 +15,7 @@ import { Utils } from "../../../Utils"; import KeyRestrictionRow from "./KeyRestrictionRow"; import { CompileScript } from "../../util/Scripting"; import { ScriptField } from "../../../new_fields/ScriptField"; +import { CollectionSchemaView } from "./CollectionSchemaView"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -156,6 +157,12 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Schema: return ( + <CollectionSchemaViewChrome + key="collchrome" + CollectionView={this.props.CollectionView} + type={this.props.type} + />); default: return null; } @@ -231,7 +238,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro {this.subChrome()} </div> </div> - ) + ); } } @@ -335,6 +342,21 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView </div> </div> </div> - ) + ); + } +} + +interface SchemaChromeProps extends CollectionViewChromeProps { + toolbar: JSX.Element; +} + +@observer +export class CollectionSchemaViewChrome extends React.Component<SchemaChromeProps> { + render() { + return ( + <div className="collectionStackingViewChrome-cont"> + {this.props.toolbar} + </div> + ); } }
\ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 1a00db1c1..bb6130992 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -283,21 +283,28 @@ export namespace Doc { export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) { if (target[key] === undefined) { + console.log("target key undefined"); Doc.GetProto(target)[key] = new List<Doc>(); } let list = Cast(target[key], listSpec(Doc)); if (list) { + console.log("has list"); if (allowDuplicates !== true) { let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1); if (pind !== -1) { list.splice(pind, 1); } } - if (first) list.splice(0, 0, doc); + if (first) { + console.log("is first"); + list.splice(0, 0, doc); + } else { + console.log("not first"); let ind = relativeTo ? list.indexOf(relativeTo) : -1; if (ind === -1) list.push(doc); else list.splice(before ? ind : ind + 1, 0, doc); + console.log("index", ind); } } return true; |