aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/CollectionSchemaView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/CollectionSchemaView.tsx')
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx454
1 files changed, 291 insertions, 163 deletions
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 04f017378..e2b90e26d 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,84 +1,131 @@
-import React = require("react")
-import { action, observable } from "mobx";
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, untracked, runInAction } from "mobx";
import { observer } from "mobx-react";
-import Measure from "react-measure";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
+import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
import "react-table/react-table.css";
-import { Document } from "../../../fields/Document";
-import { Field } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { CompileScript, ToField } from "../../util/Scripting";
+import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
+import { SetupDrag } from "../../util/DragManager";
+import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
-import { ContextMenu } from "../ContextMenu";
+import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";
+import { anchorPoints, Flyout } from "../DocumentDecorations";
+import '../DocumentDecorations.scss';
import { EditableView } from "../EditableView";
import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
-import { CollectionViewBase } from "./CollectionViewBase";
-import { setupDrag } from "../../util/DragManager";
+import { CollectionSubView } from "./CollectionSubView";
+import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { List } from "../../../new_fields/List";
+import { Id } from "../../../new_fields/RefField";
+import { Gateway } from "../../northstar/manager/Gateway";
+import { Docs } from "../../documents/Documents";
+import { ContextMenu } from "../ContextMenu";
+
// 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
-export class CollectionSchemaView extends CollectionViewBase {
- private _mainCont = React.createRef<HTMLDivElement>();
- private DIVIDER_WIDTH = 5;
-
- @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView
- @observable _dividerX = 0;
- @observable _panelWidth = 0;
- @observable _panelHeight = 0;
+class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> {
+ constructor(props: any) {
+ super(props);
+ }
+
+ render() {
+ return (
+ <div key={this.props.keyName}>
+ <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} />
+ {this.props.keyName}
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionSchemaView extends CollectionSubView(doc => doc) {
+ private _mainCont?: HTMLDivElement;
+ private _startSplitPercent = 0;
+ private DIVIDER_WIDTH = 4;
+
+ @observable _columns: Array<string> = ["title", "data", "author"];
@observable _selectedIndex = 0;
- @observable _splitPercentage: number = 50;
+ @observable _columnsPercentage = 0;
+ @observable _keys: string[] = [];
+ @observable _newKeyName: string = "";
+
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
+ @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
- doc: rowProps.value[0],
+ Document: rowProps.value[0],
fieldKey: rowProps.value[1],
- isSelected: () => false,
- select: () => { },
+ ContainingCollectionView: this.props.CollectionView,
+ isSelected: returnFalse,
+ select: emptyFunction,
isTopMost: false,
- bindings: {},
selectOnLoad: false,
- }
- let contents = (
- <FieldView {...props} />
- )
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ active: returnFalse,
+ whenActiveChanged: emptyFunction,
+ PanelHeight: returnZero,
+ PanelWidth: returnZero,
+ };
+ let fieldContentView = <FieldView {...props} />;
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = setupDrag(reference, () => props.doc);
+ let onItemDown = (e: React.PointerEvent) =>
+ (this.props.CollectionView.props.isSelected() ?
+ SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined);
+ 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 onPointerDown={onItemDown} key={props.doc.Id} ref={reference}>
- <EditableView contents={contents}
- height={36} GetValue={() => {
- let field = props.doc.Get(props.fieldKey);
- if (field && field instanceof Field) {
- return field.ToScriptString();
+ <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) {
+ //TODO Types
+ // return field.ToScriptString();
+ return String(field);
}
- return field || "";
+ return "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value, undefined, true);
+ let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
if (!script.compiled) {
return false;
}
- let field = script();
- if (field instanceof Field) {
- props.doc.Set(props.fieldKey, field);
- return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- props.doc.Set(props.fieldKey, dataField);
- return true;
- }
+ return applyToDoc(props.Document, script.run);
+ }}
+ OnFillDown={async (value: string) => {
+ let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ if (!script.compiled) {
+ return;
}
- return false;
+ 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>
- )
+ </div >
+ );
}
private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
@@ -88,159 +135,240 @@ export class CollectionSchemaView extends CollectionViewBase {
}
return {
onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
+ that.props.select(e.ctrlKey);
that._selectedIndex = rowInfo.index;
- this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize
if (handleOriginal) {
- handleOriginal()
+ handleOriginal();
}
}),
style: {
- background: rowInfo.index == this._selectedIndex ? "lightGray" : "white",
- //color: rowInfo.index == this._selectedIndex ? "white" : "black"
+ background: rowInfo.index === this._selectedIndex ? "lightGray" : "white",
+ //color: rowInfo.index === this._selectedIndex ? "white" : "black"
}
};
}
- _startSplitPercent = 0;
+ private createTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
+ super.CreateDropTarget(ele);
+ }
+
+ @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);
+ }
+ }
+ }
+
+ //toggles preview side-panel of schema
+ @action
+ toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ }
+
@action
onDividerMove = (e: PointerEvent): void => {
- let nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100);
+ let nativeWidth = this._mainCont!.getBoundingClientRect();
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startSplitPercent == this._splitPercentage) {
- this._splitPercentage = this._splitPercentage == 1 ? 66 : 100;
+ if (this._startSplitPercent === this.splitPercentage) {
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
}
}
onDividerDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this._splitPercentage;
+ this._startSplitPercent = this.splitPercentage;
e.stopPropagation();
e.preventDefault();
document.addEventListener("pointermove", this.onDividerMove);
document.addEventListener('pointerup', this.onDividerUp);
}
- @action
- onExpanderMove = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (this.props.isSelected()) e.stopPropagation();
+ else e.preventDefault();
+ }
}
- @action
- onExpanderUp = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
- document.removeEventListener("pointermove", this.onExpanderMove);
- document.removeEventListener('pointerup', this.onExpanderUp);
- if (this._startSplitPercent == this._splitPercentage) {
- this._splitPercentage = this._splitPercentage == 100 ? 66 : 100;
+
+ onWheel = (e: React.WheelEvent): void => {
+ if (this.props.active()) {
+ e.stopPropagation();
}
}
- onExpanderDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this._splitPercentage;
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onExpanderMove);
- document.addEventListener('pointerup', this.onExpanderUp);
- }
-
- onPointerDown = (e: React.PointerEvent) => {
- // if (e.button === 2 && this.active) {
- // e.stopPropagation();
- // e.preventDefault();
- // } else
- {
- if (e.buttons === 1) {
- if (this.props.isSelected()) {
- e.stopPropagation();
- }
+
+ 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 });
+ }
+ }
+
+ @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() : "") + ",", "");
+ 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.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName });
+ if (schemaDoc) {
+ self.props.CollectionView.props.addDocument(schemaDoc, false);
}
}
}
@action
- setScaling = (r: any) => {
- const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
- const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
- this._panelWidth = r.entry.width;
- this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight;
- this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width);
+ addColumn = () => {
+ this.columns.push(this._newKeyName);
+ this._newKeyName = "";
}
- getContentScaling = (): number => this._contentScaling;
- getPanelWidth = (): number => this._panelWidth;
- getPanelHeight = (): number => this._panelHeight;
- getTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling);
+ @action
+ newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._newKeyName = e.currentTarget.value;
}
- focusDocument = (doc: Document) => { }
+ @observable previewScript: string = "";
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.previewScript = e.currentTarget.value;
+ }
- render() {
- const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author])
- const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
- const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
- let content = this._selectedIndex == -1 || !selected ? (null) : (
- <Measure onResize={this.setScaling}>
- {({ measureRef }) =>
- <div className="collectionSchemaView-content" ref={measureRef}>
- <DocumentView Document={selected}
- AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument}
- isTopMost={false}
- SelectOnLoad={false}
- ScreenToLocalTransform={this.getTransform}
- ContentScaling={this.getContentScaling}
- PanelWidth={this.getPanelWidth}
- PanelHeight={this.getPanelHeight}
- ContainingCollectionView={this.props.CollectionView}
- focus={this.focusDocument}
- />
+ @computed
+ get previewDocument(): Doc | undefined {
+ const children = DocListCast(this.props.Document[this.props.fieldKey]);
+ const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
+ return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ }
+ get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); }
+ get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; }
+ get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; }
+
+ private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth);
+ private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight);
+ private previewContentScaling = () => {
+ let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth);
+ if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) {
+ return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight);
+ }
+ return wscale;
+ }
+ private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
+ private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling();
+ get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; }
+ getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset,
+ - this.borderWidth).scale(1 / this.previewContentScaling())
+
+ @computed
+ get previewPanel() {
+ // let doc = CompileScript(this.previewScript, { this: selected }, true)();
+ const previewDoc = this.previewDocument;
+ return (<div className="collectionSchemaView-previewRegion" style={{ width: `${Math.max(0, this.previewRegionWidth - 1)}px` }}>
+ {!previewDoc || !this.previewRegionWidth ? (null) : (
+ <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
+ <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false}
+ toggleMinimized={emptyFunction}
+ addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
+ ScreenToLocalTransform={this.getPreviewTransform}
+ ContentScaling={this.previewContentScaling}
+ PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight}
+ ContainingCollectionView={this.props.CollectionView}
+ focus={emptyFunction}
+ parentActive={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction}
+ />
+ </div>)}
+ <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (previewDoc ? this.previewPanelWidth() / 2 : 75))}px)` }} />
+ </div>);
+ }
+
+ 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.splitPercentage !== 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>
- }
- </Measure>
- )
- let previewHandle = !this.props.active() ? (null) : (
- <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />);
- let dividerDragger = this._splitPercentage == 100 ? (null) :
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />
+ </div>
+ }>
+ <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button>
+ </Flyout>);
+ }
+
+ @computed
+ get dividerDragger() {
+ return this.splitPercentage === 0 ? (null) :
+ <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
+ }
+
+ render() {
+ library.add(faCog);
+ library.add(faPlus);
+ const children = this.children;
return (
- <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} >
- <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
- <Measure onResize={action((r: any) => {
- this._dividerX = r.entry.width;
- this._panelHeight = r.entry.height;
- })}>
- {({ measureRef }) =>
- <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}>
- <ReactTable
- data={children}
- pageSize={children.length}
- page={0}
- showPagination={false}
- columns={columns.map(col => ({
- Header: col.Name,
- accessor: (doc: Document) => [doc, col],
- id: col.Id
- }))}
- column={{
- ...ReactTableDefaults.column,
- Cell: this.renderCell,
-
- }}
- getTrProps={this.getTrProps}
- />
- </div>
- }
- </Measure>
- {dividerDragger}
- <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}>
- {content}
- </div>
- {previewHandle}
+ <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-tableContainer" style={{ width: `${this.tableWidth}px` }}>
+ <ReactTable data={children} page={0} pageSize={children.length} showPagination={false}
+ columns={this.columns.map(col => ({
+ Header: col,
+ accessor: (doc: Doc) => [doc, col],
+ id: col
+ }))}
+ column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
+ getTrProps={this.getTrProps}
+ />
</div>
- </div >
- )
+ {this.dividerDragger}
+ {this.previewPanel}
+ {this.tableOptionsPanel}
+ </div>
+ );
}
} \ No newline at end of file