aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Wilkins <abdullah_ahmed@brown.edu>2019-07-17 14:08:24 -0400
committerSam Wilkins <abdullah_ahmed@brown.edu>2019-07-17 14:08:24 -0400
commit6c7dc0f939635982ae619eb5831ff45063d7021e (patch)
tree8b093c9f5356390e621df4626353191b5c6c53fd
parent2f9aadfce9a1d8c26457bc56b1b095ae625be77b (diff)
can add columns and delete columns through column header
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx440
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx238
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss21
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx286
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>
);
}