diff options
Diffstat (limited to 'src/client/views/collections/CollectionSchemaView.tsx')
-rw-r--r-- | src/client/views/collections/CollectionSchemaView.tsx | 782 |
1 files changed, 534 insertions, 248 deletions
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 2cf50e551..febf95dc7 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -4,15 +4,15 @@ 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, 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 } from "../../util/Scripting"; @@ -31,6 +31,11 @@ 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"; @@ -39,22 +44,20 @@ 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 - -@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([ + ["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) { @@ -62,148 +65,42 @@ 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 = this.childDocs.length ? this.childDocs[0] : undefined; + @observable private _node : HTMLDivElement | null = null; + @observable private _focusedTable: Doc = this.props.Document; @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 - }; - }); - } - 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; - } - - 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 createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + super.CreateDropTarget(ele); } - private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { - const that = this; - if (!rowInfo) { - return {}; - } - return { - onClick: action((e: React.MouseEvent, handleOriginal: Function) => { - that.props.select(e.ctrlKey); - that._selectedIndex = rowInfo.index; + // detectClick = (e: PointerEvent): void => { + // if (this._node && this._node.contains(e.target as Node)) { + // } else { + // this._isOpen = false; + // this.props.setIsEditing(false); + // } + // } - if (handleOriginal) { - handleOriginal(); - } - }), - style: { - background: rowInfo.index === this._selectedIndex ? "lightGray" : "white", - //color: rowInfo.index === this._selectedIndex ? "white" : "black" - } - }; + isFocused = (doc: Doc): boolean => { + if (!this.props.isSelected()) return false; + return doc === this._focusedTable; } - private createTarget = (ele: HTMLDivElement) => { - this._mainCont = ele; - super.CreateDropTarget(ele); + @action + setFocused = (doc: Doc): void => { + this._focusedTable = doc; } @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); - } - } + setPreviewDoc = (doc: Doc): void => { + this.previewDoc = doc; } //toggles preview side-panel of schema @@ -246,56 +143,460 @@ 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 }); - } + @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; + 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 - 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"; + 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} + /> + ); + } + + @computed + get schemaToolbar() { + return ( + <div className="collectionSchemaView-toolbar"> + <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div> + </div> + ); + } + + render() { + + // 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 ( + <> + {this.schemaToolbar} + <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 + PanelHeight: () => number; + PanelWidth: () => number; + childDocs: Doc[]; + CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + fieldKey: string; + renderDepth: number; + 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; +} + +@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<Doc> = []; + @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(); + + if (this.props.childDocs.reduce((found, doc) => found || doc.type === "collection", false)) { + columns.push( + { + expander: true, + Header: "", + width: 45, + Expander: (rowInfo) => { + if (rowInfo.original.type === "collection") { + if (rowInfo.isExpanded) return <div onClick={() => this.onCloseCollection(rowInfo.original)}>-</div>; + if (!rowInfo.isExpanded) return <div onClick={() => this.onExpandCollection(rowInfo.original)}>+</div>; + } else { + return null; + } + } + } + ); + } + + 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, + }; }); - 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; + columns.push(...cols); + + columns.push({ + Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, + accessor: (doc: Doc) => 0, + id: "add", + Cell: (rowProps: CellInfo) => <></>, + width: 45, + resizable: false + }); + return columns; + } + + // onHeaderDrag = (columnName: string) => { + // let schemaDoc = Cast(this.props.Document.schemaDoc, Doc); + // if (schemaDoc instanceof Doc) { + // let columnDocs = DocListCast(schemaDoc.data); + // if (columnDocs) { + // let ddoc = columnDocs.find(doc => doc.title === columnName); + // if (ddoc) { + // return ddoc; + // } + // } + // } + // return this.props.Document; + // } + + componentDidMount() { + document.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); + } + + tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + console.log("table add doc"); + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + } + + tableMoveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => { + console.log("SCHEMA MOVE"); + this.props.moveDocument(d, target, addDoc); + } + + private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { + const that = this; + if (!rowInfo) { + return {}; + } + return { + ScreenToLocalTransform: this.props.ScreenToLocalTransform, + addDoc: this.tableAddDoc, + moveDoc: this.tableMoveDoc, + rowInfo, + rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document) + }; + } + + // 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 + onExpandCollection = (collection: Doc): void => { + this._openCollections.push(collection); + } + + @action + onCloseCollection = (collection: Doc): void => { + let index = this._openCollections.findIndex(col => col === collection); + if (index > -1) this._openCollections.splice(index, 1); + } + + @action + setCellIsEditing = (isEditing: boolean): void => { + this._cellIsEditing = isEditing; + } + + @action + setHeaderIsEditing = (isEditing: boolean): void => { + this._headerIsEditing = isEditing; + } + + onPointerDown = (e: React.PointerEvent): void => { + // console.log("pointer down", StrCast(this.props.Document.title)); + this.props.setFocused(this.props.Document); + if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (this.props.isSelected()) e.stopPropagation(); + } + } + + onWheel = (e: React.WheelEvent): void => { + if (this.props.active()) { + e.stopPropagation(); + } + } + + 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 + changeFocusedCellByDirection = (direction: string): void => { + switch (direction) { + case "tab": + if (this._focusedCell.col + 1 === this.columns.length && this._focusedCell.row + 1 === this.props.childDocs.length) { + this._focusedCell = { row: 0, col: 0 }; + } else if (this._focusedCell.col + 1 === this.columns.length) { + this._focusedCell = { row: this._focusedCell.row + 1, col: 0 }; + } else { + this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 }; + } + break; + case "right": + this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 === this.columns.length ? this._focusedCell.col : this._focusedCell.col + 1 }; + break; + case "left": + this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col === 0 ? this._focusedCell.col : this._focusedCell.col - 1 }; + break; + case "up": + this._focusedCell = { row: this._focusedCell.row === 0 ? this._focusedCell.row : this._focusedCell.row - 1, col: this._focusedCell.col }; + break; + case "down": + this._focusedCell = { row: this._focusedCell.row + 1 === this.props.childDocs.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col }; + break; + } + } + + @action + changeFocusedCellByIndex = (row: number, col: number): void => { + this._focusedCell = { row: row, col: col }; + this.props.setFocused(this.props.Document); + // console.log("changed cell by index", StrCast(this.props.Document.title)); + } + + @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. @@ -307,101 +608,84 @@ 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)); } @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 hasCollectionChild = this.props.childDocs.reduce((found, doc) => found || doc.type === "collection", false); + let expandedRowsList = this._openCollections.map(col => this.props.childDocs.findIndex(doc => doc === col).toString()); + let expanded = {}; + expandedRowsList.forEach(row => expanded[row] = true); + + return <ReactTable + style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} + data={this.props.childDocs} + page={0} + pageSize={this.props.childDocs.length} + showPagination={false} columns={this.tableColumns} - column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} + sortable={false} + TrComponent={MovableRow} + sorted={Array.from(this._sortedColumns.values())} + 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} childDocs={childDocs}/></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} - 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} </div> ); } } + + interface CollectionSchemaPreviewProps { Document?: Doc; - DataDocument?: Doc; + DataDocument?: Doc; childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; @@ -477,6 +761,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} |