diff options
author | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-07-17 14:08:24 -0400 |
---|---|---|
committer | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-07-17 14:08:24 -0400 |
commit | 6c7dc0f939635982ae619eb5831ff45063d7021e (patch) | |
tree | 8b093c9f5356390e621df4626353191b5c6c53fd /src | |
parent | 2f9aadfce9a1d8c26457bc56b1b095ae625be77b (diff) |
can add columns and delete columns through column header
Diffstat (limited to 'src')
4 files changed, 880 insertions, 105 deletions
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx new file mode 100644 index 000000000..f15734df6 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -0,0 +1,440 @@ +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 } from "../../../new_fields/Types"; + + +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; + changeFocusedCellByDirection: (direction: string) => void; + 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; + + componentDidMount() { + if (this._focusRef.current) { + if (this.props.isFocused) { + this._focusRef.current.className += " focused"; + } else { + this._focusRef.current.className = "collectionSchemaView-cellWrapper"; + } + } + + document.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); + } + + @action + onKeyDown = (e: KeyboardEvent): void => { + if (this.props.isFocused && this.props.isEditable) { + // console.log("schema cell", 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); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + this.props.changeFocusedCellByIndex(this.props.row, this.props.col); + } + + 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, run: (args?: { [name: string]: any }) => any) => { + const res = run({ this: doc }); + if (!res.success) return false; + doc[props.fieldKey] = res.result; + return true; + }; + 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) => { + let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } }); + if (!script.compiled) { + return false; + } + return applyToDoc(props.Document, script.run); + }} + OnFillDown={async (value: string) => { + let script = CompileScript(value, { 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 => applyToDoc(doc, run)); + }}> + </EditableView> + </div > + ); + } + + renderCellWithType(type: string | undefined) { + 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 fieldContentView: JSX.Element = <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, run: (args?: { [name: string]: any }) => any) => { + const res = run({ this: doc }); + if (!res.success) return false; + doc[props.fieldKey] = res.result; + return true; + }; + + let field = props.Document[props.fieldKey]; + let contents = type === undefined ? <FieldView {...props} /> : type === "number" ? NumCast(field) : type === "boolean" ? (BoolCast(field) ? "true" : "false") : "incorrect type"; + // let contents = typeof field === "number" ? NumCast(field) : "incorrect type"; + + return ( + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> + <EditableView + editing={this._isEditing} + // isEditingCallback={this.isEditingCallback} + 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) => { + let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name } }); + if (!script.compiled) { + return false; + } + return 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 => applyToDoc(doc, run)); + }} /> + </div > + </div> + ); + } + + render() { + return this.renderCell(this.props.rowProps); + } +} + +@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 CollectionSchemaCheckboxCell extends CollectionSchemaCell { + render() { + console.log("render checkbox cell"); + 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 fieldContentView: JSX.Element = <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, run: (args?: { [name: string]: any }) => any) => { + const res = run({ this: doc }); + if (!res.success) return false; + doc[props.fieldKey] = res.result; + return true; + }; + + let field = props.Document[props.fieldKey]; + let contents = BoolCast(field); + console.log("contents", contents); + // let contents = typeof field === "number" ? NumCast(field) : "incorrect type"; + + let toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { + console.log("toggle check", e.target.checked); + // this._isChecked = e.target.checked; + + let document = this.props.rowProps.original; + let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); + if (script.compiled) { + let applied = applyToDoc(document, script.run); + console.log("applied", applied); + } + }; + + return ( + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> + <input type="checkbox" onChange={toggleChecked} /> + {/* <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + 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) => { + let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name } }); + if (!script.compiled) { + return false; + } + return 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 => applyToDoc(doc, run)); + }} /> */} + </div > + </div> + ); + } +} + + // @observer +// export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { +// // @observable private _isChecked: boolean = BoolCast(this.props.rowProps.original[this.props.fieldKey]); + +// render() { +// console.log("checkbox rneder"); + +// 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 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, run: (args?: { [name: string]: any }) => any) => { +// const res = run({ this: doc }); +// if (!res.success) return false; +// doc[this.props.fieldKey] = res.result; +// return true; +// }; + +// let toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { +// console.log("toggle check", e.target.checked); +// // this._isChecked = e.target.checked; + +// let document = this.props.rowProps.original; +// let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); +// if (script.compiled) { +// console.log("script compiled"); +// applyToDoc(document, script.run); +// } +// }; + + +// let field = props.Document[props.fieldKey]; +// // let contents = typeof field === "boolean" ? BoolCast(field) : "incorrect type"; +// let checked = typeof field === "boolean" ? BoolCast(field) : false; + +// return ( +// <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> +// <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> +// <input type="checkbox" checked={checked} onChange={toggleChecked} /> + +// {/* <EditableView +// editing={this._isEditing} +// isEditingCallback={this.isEditingCallback} +// display={"inline"} +// contents={contents} +// height={Number(MAX_ROW_HEIGHT)} +// GetValue={() => { +// let field = props.Document[props.fieldKey]; +// if (typeof field === "string") { +// return Field.toScriptString(field); +// } +// return ""; +// } +// } +// SetValue={(value: string) => { +// let script = CompileScript(value, { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); +// if (!script.compiled) { +// return false; +// } +// return applyToDoc(props.Document, script.run); +// }} +// OnFillDown={async (value: string) => { +// let script = CompileScript(value, { requiredType: "boolean", 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 => applyToDoc(doc, run)); +// }} /> */} +// </div > +// </div> +// ); +// } +// }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx new file mode 100644 index 000000000..a9d4a0170 --- /dev/null +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -0,0 +1,238 @@ +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 } 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"; + +library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare); + +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; +} + +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.Checkbox || this.props.keyType === ColumnType.Boolean ? "check-square" : "align-justify"; + + return ( + <div className="collectionSchemaView-header" > + <CollectionSchemaColumnMenu + keyValue={this.props.keyValue} + possibleKeys={this.props.possibleKeys} + existingKeys={this.props.existingKeys} + keyType={this.props.keyType} + 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} + /> + </div> + ); + } +} + + +export interface AddColumnHeaderProps { + possibleKeys: string[]; + existingKeys: string[]; + onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + setIsEditing: (isEditing: boolean) => void; +} + +@observer +export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> { + // @observable private _creatingColumn: boolean = false; + + // @action + // onClick = (e: React.MouseEvent): void => { + // this._creatingColumn = true; + // } + + render() { + let addButton = <button onClick={() => console.log("add clicked")}><FontAwesomeIcon icon="plus" size="sm" /></button>; + return ( + <div className="collectionSchemaView-header-addColumn" > + {/* {this._creatingColumn ? <></> : */} + <CollectionSchemaColumnMenu + keyValue="" + possibleKeys={this.props.possibleKeys} + existingKeys={this.props.existingKeys} + keyType={ColumnType.Any} + menuButtonContent={addButton} + addNew={true} + onSelect={this.props.onSelect} + setIsEditing={this.props.setIsEditing} + deleteColumn={action(emptyFunction)} + onlyShowOptions={true} + setColumnType={action(emptyFunction)} + /> + </div> + ); + } +} + + + +export interface ColumnMenuProps { + keyValue: string; + possibleKeys: string[]; + existingKeys: string[]; + keyType: ColumnType; + 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; +} +@observer +export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> { + @observable private _isOpen: boolean = 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); + } + + renderContent = () => { + let keyTypeStr = ColumnType[this.props.keyType]; + let colTypes = []; + for (let type in ColumnType) { + if (!(parseInt(type, 10) >= 0)) colTypes.push(type); + } + + if (this._isOpen) { + if (this.props.onlyShowOptions) { + return ( + <div className="collectionSchema-header-menuOptions"> + <KeysDropdown + keyValue={this.props.keyValue} + possibleKeys={this.props.possibleKeys} + existingKeys={this.props.existingKeys} + canAddNew={true} + addNew={this.props.addNew} + onSelect={this.props.onSelect} + /> + </div> + ); + } else { + return ( + <div className="collectionSchema-header-menuOptions"> + <KeysDropdown + keyValue={this.props.keyValue} + possibleKeys={this.props.possibleKeys} + existingKeys={this.props.existingKeys} + canAddNew={true} + addNew={this.props.addNew} + onSelect={this.props.onSelect} + /> + <KeysDropdown + keyValue={keyTypeStr} + possibleKeys={colTypes} + existingKeys={[]} + canAddNew={false} + addNew={false} + onSelect={this.setColumnType} + /> + <button onClick={() => this.props.deleteColumn(this.props.keyValue)}>Delete Column</button> + </div> + ); + } + } + } + + render() { + return ( + // <Flyout anchorPoint={anchorPoints.TOP} content={<div style={{ color: "black" }}>{this.renderContent()}</div>}> + // <div onClick={() => { this.props.setIsEditing(true); console.log("clicked anchor"); }}>{this.props.menuButton}</div> + // </ Flyout > + <div className="collectionSchema-header-menu"> + <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div> + {this.renderContent()} + </div> + ); + } +} + + +interface KeysDropdownProps { + keyValue: string; + possibleKeys: string[]; + existingKeys: string[]; + canAddNew: boolean; + addNew: boolean; + onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + +} +@observer +class KeysDropdown extends React.Component<KeysDropdownProps> { + @observable private _key: string = this.props.keyValue; + @observable private _searchTerm: string = ""; + + @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; + @action setKey = (key: string): void => { this._key = key; }; + + @action + onSelect = (key: string): void => { + this.props.onSelect(this._key, key, this.props.addNew); + this.setKey(key); + } + + onChange = (val: string): void => { + this.setSearchTerm(val); + } + + renderOptions = (): JSX.Element[] | JSX.Element => { + 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 type="text" value={this._searchTerm} placeholder="Search for or create a new key" + onChange={e => this.onChange(e.target.value)} ></input> + <div className="keys-options-wrapper"> + {this.renderOptions()} + </div> + </div > + ); + } +} diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 186e006f3..4ab38b9d9 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -187,6 +187,27 @@ .documentView-node:first-child { background: $light-color; } + + .ReactTable .rt-thead .rt-resizable-header:last-child { + overflow: visible; + } +} + +.collectionSchema-header-menuOptions { + position: absolute; + top: $MAX_ROW_HEIGHT; + left: 0; + z-index: 9999; + background-color: $light-color-secondary; + color: black; + border: 1px solid $main-accent; + width: 250px; + padding: 10px; + + input { + color: black; + width: 100%; + } } //options menu styling diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index f72b1aa07..8ddf26be2 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -4,10 +4,10 @@ import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; -import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, TableCellRenderer } 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 } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; @@ -31,12 +31,28 @@ 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 } from "./CollectionSchemaCells"; library.add(faCog); library.add(faPlus); // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 +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([ + ["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 class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> { @@ -66,21 +82,72 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @observable _keys: string[] = []; @observable _newKeyName: string = ""; @observable previewScript: string = ""; + @observable _headerIsEditing: boolean = false; @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() { - return this.columns.map(col => { - const ref = React.createRef<HTMLParagraphElement>(); + let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.toUpperCase() === key.toUpperCase()) === -1); + + let cols = this.columns.map(col => { return { - Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>, + Header: <CollectionSchemaHeader + keyValue={col} + possibleKeys={possibleKeys} + existingKeys={this.columns} + keyType={this.getColumnType(col)} + typeConst={false} + onSelect={this.changeColumns} + setIsEditing={this.setHeaderIsEditing} + deleteColumn={this.deleteColumn} + setColumnType={this.setColumnType} + />, accessor: (doc: Doc) => doc ? doc[col] : 0, - id: col + id: col, + Cell: (rowProps: CellInfo) => { + let row = rowProps.index; + let column = this.columns.indexOf(rowProps.column.id!); + // let isFocused = focusedRow === row && focusedCol === column; + let isFocused = false; + + let props: CellProps = { + row: row, + col: column, + rowProps: rowProps, + isFocused: isFocused, + changeFocusedCellByDirection: action(emptyFunction),//this.changeFocusedCellByDirection, + changeFocusedCellByIndex: action(emptyFunction), //this.changeFocusedCellByIndex, + CollectionView: this.props.CollectionView, + ContainingCollection: this.props.ContainingCollectionView, + Document: this.props.Document, + fieldKey: this.props.fieldKey, + renderDepth: this.props.renderDepth, addDocTab: this.props.addDocTab, + moveDocument: this.props.moveDocument, + setIsEditing: action(emptyFunction), //this.setCellIsEditing, + isEditable: true //isEditable + }; + return <CollectionSchemaCell {...props}/> + } }; + }) as {Header: TableCellRenderer, accessor: (doc: Doc) => FieldResult<Field>, id: string, Cell: (rowProps: CellInfo) => JSX.Element}[]; + + cols.push({ + Header: <CollectionSchemaAddColumnHeader + possibleKeys={possibleKeys} + existingKeys={this.columns} + onSelect={this.changeColumns} + setIsEditing={this.setHeaderIsEditing} + />, + accessor: (doc: Doc) => 0, + id: "add", + Cell: (rowProps: CellInfo) => <></>, }); + + return cols; } onHeaderDrag = (columnName: string) => { @@ -97,72 +164,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return this.props.Document; } - 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, run: (args?: { [name: string]: any }) => any) => { - const res = run({ this: doc }); - if (!res.success) return false; - doc[props.fieldKey] = res.result; - return true; - }; - 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) => { - let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } }); - if (!script.compiled) { - return false; - } - return applyToDoc(props.Document, script.run); - }} - OnFillDown={async (value: string) => { - let script = CompileScript(value, { 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 => applyToDoc(doc, run)); - }}> - </EditableView> - </div > - ); - } - private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { const that = this; if (!rowInfo) { @@ -190,6 +191,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @action + setHeaderIsEditing = (isEditing: boolean) => { + this._headerIsEditing = isEditing; + } + + @action toggleKey = (key: string) => { let list = Cast(this.props.Document.schemaColumns, listSpec("string")); if (list === undefined) { @@ -278,10 +284,60 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @action - newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._newKeyName = e.currentTarget.value; + 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 + 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; + // } + @computed get previewDocument(): Doc | undefined { const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined; @@ -289,11 +345,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return pdc; } - getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( - - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth) + getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); - 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. @@ -305,39 +360,60 @@ 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)); + } + + // get documentKeysCheckList() { + // const docs = DocListCast(this.props.Document[this.props.fieldKey]); + // let keys: { [key: string]: boolean } = {}; + // // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. + // // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be + // // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. + // // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu + // // is displayed (unlikely) it won't show up until something else changes. + // //TODO Types + // untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); + + // this.columns.forEach(key => keys[key] = true); + // return Array.from(Object.keys(keys)).map(item => + // (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />)); + // } + + // get tableOptionsPanel() { + // return !this.props.active() ? (null) : + // (<Flyout + // anchorPoint={anchorPoints.RIGHT_TOP} + // content={<div> + // <div id="schema-options-header"><h5><b>Options</b></h5></div> + // <div id="options-flyout-div"> + // <h6 className="schema-options-subHeader">Preview Window</h6> + // <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div> + // <h6 className="schema-options-subHeader" >Displayed Columns</h6> + // <ul id="schema-col-checklist" > + // {this.documentKeysCheckList} + // </ul> + // <input value={this._newKeyName} onChange={this.newKeyChange} /> + // <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> + // </div> + // </div> + // }> + // <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> + // </Flyout>); + // } @computed get reactTable() { let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1; - return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false} + return <ReactTable + style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} + data={this.childDocs} + page={0} + pageSize={this.childDocs.length} + showPagination={false} columns={this.tableColumns} - column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} + // column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} + sortable={false} />; } @@ -392,7 +468,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { {this.reactTable} {this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} - {this.tableOptionsPanel} + {/* {this.tableOptionsPanel} */} </div> ); } |