diff options
Diffstat (limited to 'src/client')
15 files changed, 2306 insertions, 1891 deletions
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 99d50b4a2..4fd145f2d 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -180,6 +180,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { }) .filter(m => m) .map(m => m!); + console.log(LightboxView._tourMap); } @action public static Previous() { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx deleted file mode 100644 index 9653f2808..000000000 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ /dev/null @@ -1,513 +0,0 @@ -import React = require("react"); -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction, trace } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc"; -import { listSpec } from "../../../../fields/Schema"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ScriptField } from "../../../../fields/ScriptField"; -import { Cast, StrCast } from "../../../../fields/Types"; -import { undoBatch } from "../../../util/UndoManager"; -import { CollectionView } from "../CollectionView"; -import { ColumnType } from "./CollectionSchemaView"; -import "./CollectionSchemaView.scss"; - -const higflyout = require("@hig/flyout"); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - - -export interface AddColumnHeaderProps { - createColumn: () => void; -} - -@observer -export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> { - // the button that allows the user to add a column - render() { - return <button className="add-column" onClick={() => this.props.createColumn()}> - <FontAwesomeIcon icon="plus" size="sm" /> - </button>; - } -} - -export interface ColumnMenuProps { - columnField: SchemaHeaderField; - // keyValue: string; - possibleKeys: string[]; - existingKeys: string[]; - // keyType: ColumnType; - typeConst: boolean; - menuButtonContent: JSX.Element; - addNew: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; - setIsEditing: (isEditing: boolean) => void; - deleteColumn: (column: string) => void; - onlyShowOptions: boolean; - setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; - setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; - anchorPoint?: any; - setColumnColor: (column: SchemaHeaderField, color: string) => void; -} -@observer -export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> { - @observable private _isOpen: boolean = false; - @observable private _node: HTMLDivElement | null = null; - - componentDidMount() { document.addEventListener("pointerdown", this.detectClick); } - - componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); } - - @action - detectClick = (e: PointerEvent) => { - !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false); - } - - @action - toggleIsOpen = (): void => { - this.props.setIsEditing(this._isOpen = !this._isOpen); - } - - changeColumnType = (type: ColumnType) => { - this.props.setColumnType(this.props.columnField, type); - } - - changeColumnSort = (desc: boolean | undefined) => { - this.props.setColumnSort(this.props.columnField, desc); - } - - changeColumnColor = (color: string) => { - this.props.setColumnColor(this.props.columnField, color); - } - - @action - setNode = (node: HTMLDivElement): void => { - if (node) { - this._node = node; - } - } - - renderTypes = () => { - if (this.props.typeConst) return (null); - - const type = this.props.columnField.type; - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Column type:</label> - <div className="columnMenu-types"> - <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}> - <FontAwesomeIcon icon={"align-justify"} size="sm" /> - Any - </div> - <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}> - <FontAwesomeIcon icon={"hashtag"} size="sm" /> - Number - </div> - <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}> - <FontAwesomeIcon icon={"font"} size="sm" /> - Text - </div> - <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}> - <FontAwesomeIcon icon={"check-square"} size="sm" /> - Checkbox - </div> - <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List)}> - <FontAwesomeIcon icon={"list-ul"} size="sm" /> - List - </div> - <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}> - <FontAwesomeIcon icon={"file"} size="sm" /> - Document - </div> - <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image)}> - <FontAwesomeIcon icon={"image"} size="sm" /> - Image - </div> - <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date)}> - <FontAwesomeIcon icon={"calendar"} size="sm" /> - Date - </div> - </div> - </div > - ); - } - - renderSorting = () => { - const sort = this.props.columnField.desc; - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Sort by:</label> - <div className="columnMenu-sort"> - <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}> - <FontAwesomeIcon icon="sort-amount-down" size="sm" /> - Sort descending - </div> - <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}> - <FontAwesomeIcon icon="sort-amount-up" size="sm" /> - Sort ascending - </div> - <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}> - <FontAwesomeIcon icon="times" size="sm" /> - Clear sorting - </div> - </div> - </div> - ); - } - - renderColors = () => { - const selected = this.props.columnField.color; - - const pink = PastelSchemaPalette.get("pink2"); - const purple = PastelSchemaPalette.get("purple2"); - const blue = PastelSchemaPalette.get("bluegreen1"); - const yellow = PastelSchemaPalette.get("yellow4"); - const red = PastelSchemaPalette.get("red2"); - const gray = "#f1efeb"; - - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Color:</label> - <div className="columnMenu-colors"> - <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div> - <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div> - <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div> - <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div> - <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div> - <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div> - </div> - </div> - ); - } - - renderContent = () => { - return ( - <div className="collectionSchema-header-menuOptions"> - {this.props.onlyShowOptions ? <></> : - <> - {this.renderTypes()} - {this.renderSorting()} - {this.renderColors()} - <div className="collectionSchema-headerMenu-group"> - <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide Column</button> - </div> - </> - } - </div> - ); - } - - render() { - return ( - <div className="collectionSchema-header-menu" ref={this.setNode}> - <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}> - <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div> - </ Flyout > - </div> - ); - } -} - - -export interface KeysDropdownProps { - keyValue: string; - possibleKeys: string[]; - existingKeys: string[]; - canAddNew: boolean; - addNew: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void; - setIsEditing: (isEditing: boolean) => void; - width?: string; - docs?: Doc[]; - Document: Doc; - dataDoc: Doc | undefined; - fieldKey: string; - ContainingCollectionDoc: Doc | undefined; - ContainingCollectionView: Opt<CollectionView>; - active?: (outsideReaction?: boolean) => boolean | undefined; - openHeader: (column: any, screenx: number, screeny: number) => void; - col: SchemaHeaderField; - icon: IconProp; -} -@observer -export class KeysDropdown extends React.Component<KeysDropdownProps> { - @observable private _key: string = this.props.keyValue; - @observable private _searchTerm: string = this.props.keyValue + ":"; - @observable private _isOpen: boolean = false; - @observable private _node: HTMLDivElement | null = null; - @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); - - @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; - @action setKey = (key: string): void => { this._key = key; }; - @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; }; - - @action - onSelect = (key: string): void => { - this.props.onSelect(this._key, key, this.props.addNew); - this.setKey(key); - this._isOpen = false; - this.props.setIsEditing(false); - } - - @action - setNode = (node: HTMLDivElement): void => { - if (node) { - this._node = node; - } - } - - componentDidMount() { - document.addEventListener("pointerdown", this.detectClick); - const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters?.some(filter => filter.split(":")[0] === this._key)) { - runInAction(() => this.closeResultsVisibility = "contents"); - } - } - - @action - detectClick = (e: PointerEvent): void => { - if (this._node && this._node.contains(e.target as Node)) { - } else { - this._isOpen = false; - this.props.setIsEditing(false); - } - } - - private tempfilter: string = ""; - @undoBatch - onKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === "Enter") { - e.stopPropagation(); - if (this._searchTerm.includes(":")) { - const colpos = this._searchTerm.indexOf(":"); - const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); - if (temp === "") { - Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove"); - this.updateFilter(); - } - else { - Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove"); - this.tempfilter = temp; - Doc.setDocFilter(this.props.Document, this._key, temp, "check"); - this.props.col.setColor("green"); - this.closeResultsVisibility = "contents"; - } - } - else { - Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove"); - this.updateFilter(); - if (this.showKeys.length) { - this.onSelect(this.showKeys[0]); - } else if (this._searchTerm !== "" && this.props.canAddNew) { - this.setSearchTerm(this._searchTerm || this._key); - this.onSelect(this._searchTerm); - } - } - } - } - - onChange = (val: string): void => { - this.setSearchTerm(val); - } - - @action - onFocus = (e: React.FocusEvent): void => { - this._isOpen = true; - this.props.setIsEditing(true); - } - - @computed get showKeys() { - const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"]; - const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const showKeys = new Set<string>(); - [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.noviceMode || - whitelistKeys.includes(key) - || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null); - return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm)); - } - - @computed get renderOptions() { - if (!this._isOpen) { - this.defaultMenuHeight = 0; - return (null); - } - const options = this.showKeys.map(key => { - return <div key={key} className="key-option" style={{ - border: "1px solid lightgray", - width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", - }} - onPointerDown={e => { - e.stopPropagation(); - }} - 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 (this._key !== this._searchTerm.slice(0, this._key.length)) { - if (this._searchTerm !== "" && this.props.canAddNew) { - options.push(<div key={""} className="key-option" style={{ - border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", - }} - onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> - Create "{this._searchTerm}" key</div>); - } - } - - if (options.length === 0) { - this.defaultMenuHeight = 0; - } - else { - if (this.props.docs) { - const panesize = this.props.docs.length * 30; - options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8; - } - else { - options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8; - } - } - return options; - } - - @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); } - - @computed get renderFilterOptions() { - if (!this._isOpen || !this.props.dataDoc) { - this.defaultMenuHeight = 0; - return (null); - } - const keyOptions: string[] = []; - const colpos = this._searchTerm.indexOf(":"); - const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); - this.docSafe.forEach(doc => { - const key = StrCast(doc[this._key]); - if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { - keyOptions.push(key); - } - }); - - const filters = StrListCast(this.props.Document._docFilters); - if (filters.some(filter => filter.split(":")[0] === this._key) === false) { - this.props.col.setColor("rgb(241, 239, 235)"); - this.closeResultsVisibility = "none"; - } - for (let i = 0; i < (filters?.length ?? 0) - 1; i++) { - if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) { - keyOptions.push(filters[i + 1]); - } - } - const options = keyOptions.map(key => { - let bool = false; - if (filters !== undefined) { - const ind = filters.findIndex(filter => filter.split(":")[1] === key); - const fields = ind === -1 ? undefined : filters[ind].split(":"); - bool = fields ? fields[2] === "check" : false; - } - return <div key={key} className="key-option" style={{ - paddingLeft: 5, textAlign: "left", - width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", - }} - > - <input type="checkbox" - onPointerDown={e => e.stopPropagation()} - onClick={e => e.stopPropagation()} - onChange={action(e => { - if (e.target.checked) { - Doc.setDocFilter(this.props.Document, this._key, key, "check"); - this.closeResultsVisibility = "contents"; - this.props.col.setColor("green"); - } else { - Doc.setDocFilter(this.props.Document, this._key, key, "remove"); - this.updateFilter(); - } - })} - checked={bool} - /> - <span style={{ paddingLeft: 4 }}> - {key} - </span> - - </div>; - }); - if (options.length === 0) { - this.defaultMenuHeight = 0; - } - else { - if (this.props.docs) { - const panesize = this.props.docs.length * 30; - options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8; - } - else { - options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8; - } - - } - return options; - } - - @observable defaultMenuHeight = 0; - - - updateFilter() { - const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { - this.props.col.setColor("rgb(241, 239, 235)"); - this.closeResultsVisibility = "none"; - } - } - - @computed get scriptField() { - const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; - const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); - return script ? () => script : undefined; - } - filterBackground = () => "rgba(105, 105, 105, 0.432)"; - @observable filterOpen: boolean | undefined = undefined; - closeResultsVisibility: string = "none"; - - removeFilters = (e: React.PointerEvent): void => { - const keyOptions: string[] = []; - this.docSafe.forEach(doc => { - const key = StrCast(doc[this._key]); - if (keyOptions.includes(key) === false) { - keyOptions.push(key); - } - }); - - Doc.setDocFilter(this.props.Document, this._key, "", "remove"); - this.props.col.setColor("rgb(241, 239, 235)"); - this.closeResultsVisibility = "none"; - } - render() { - return ( - <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}> - <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}> - <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} /> - </div> - - <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}> - <input className="keys-search" style={{ width: "100%" }} - ref={this._inputRef} type="text" - value={this._searchTerm} placeholder="Column key" - onKeyDown={this.onKeyDown} - onChange={e => this.onChange(e.target.value)} - onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} - onFocus={this.onFocus} ></input> - <div style={{ display: this.closeResultsVisibility }}> - <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg" - style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} /> - </div> - {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{ - width: this.props.width, maxWidth: this.props.width, height: "auto", - }}> - {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions} - </div>} - </div > - </div> - ); - } -} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index 19401c7f0..0f4053127 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -1,599 +1,58 @@ @import '../../global/globalCssVariables.scss'; -@import '../../../../../node_modules/react-table/react-table.css'; -.collectionSchemaView-container { - border-width: $COLLECTION_BORDER_WIDTH; - border-color: $medium-gray; - border-style: solid; - border-radius: $border-radius; - box-sizing: border-box; - position: relative; - top: 0; - width: 100%; - height: 100%; - margin-top: 0; - transition: top 0.5s; - display: flex; - justify-content: space-between; - flex-wrap: nowrap; - touch-action: none; - div { - touch-action: none; - } - .collectionSchemaView-tableContainer { - width: 100%; - height: 100%; - } - .collectionSchemaView-dividerDragger { - position: relative; - height: 100%; - width: $SCHEMA_DIVIDER_WIDTH; - z-index: 20; - right: 0; - top: 0; - background: gray; - cursor: col-resize; - } - // .documentView-node:first-child { - // background: $white; - // } -} - -.collectionSchemaView-searchContainer { - border-width: $COLLECTION_BORDER_WIDTH; - border-color: $medium-gray; - border-style: solid; - border-radius: $border-radius; - box-sizing: border-box; - position: relative; - top: 0; - width: 100%; - height: 100%; - margin-top: 0; - transition: top 0.5s; - display: flex; - justify-content: space-between; - flex-wrap: nowrap; - touch-action: none; - padding: 2px; - div { - touch-action: none; - } - .collectionSchemaView-tableContainer { - width: 100%; - height: 100%; - } - .collectionSchemaView-dividerDragger { - position: relative; - height: 100%; - width: 20px; - z-index: 20; - right: 0; - top: 0; - background: gray; - cursor: col-resize; - } - // .documentView-node:first-child { - // background: $white; - // } -} - -.ReactTable { - width: 100%; - background: white; - box-sizing: border-box; - border: none !important; - float: none !important; - .rt-table { - height: 100%; - display: -webkit-inline-box; - direction: ltr; - overflow: visible; - } - .rt-noData { - display: none; - } - .rt-thead { - width: 100%; - z-index: 100; - overflow-y: visible; - &.-header { - font-size: 12px; - height: 30px; - box-shadow: none; - z-index: 100; - overflow-y: visible; - } - .rt-resizable-header-content { - height: 100%; - overflow: visible; - } - .rt-th { - padding: 0; - border-left: solid 1px $light-gray; - } - } - .rt-th { - font-size: 13px; - text-align: center; - &:last-child { - overflow: visible; - } - } - .rt-tbody { - width: 100%; - direction: rtl; - overflow: visible; - .rt-td { - border-right: 1px solid rgba(0, 0, 0, 0.2); - } - } - .rt-tr-group { - direction: ltr; - flex: 0 1 auto; - min-height: 30px; - border: 0 !important; - } - .rt-tr-group:nth-of-type(even) { - direction: ltr; - flex: 0 1 auto; - min-height: 30px; - border: 0 !important; - background-color: red; - } - .rt-tr { - width: 100%; - min-height: 30px; - } - .rt-td { - padding: 0; - font-size: 13px; - text-align: center; - white-space: nowrap; - display: flex; - align-items: center; - .imageBox-cont { - position: relative; - max-height: 100%; - } - .imageBox-cont img { - object-fit: contain; - max-width: 100%; - height: 100%; - } - .videoBox-cont { - object-fit: contain; - width: auto; - height: 100%; - } - } - .rt-td.rt-expandable { - display: flex; - align-items: center; - height: inherit; - } - .rt-resizer { - width: 8px; - right: -4px; - } - .rt-resizable-header { - padding: 0; - height: 30px; - } - .rt-resizable-header:last-child { - overflow: visible; - .rt-resizer { - width: 5px !important; - } - } -} - -.documentView-node-topmost { - text-align: left; - transform-origin: center top; - display: inline-block; -} - -.collectionSchema-col { - height: 100%; -} - -.collectionSchema-header-menu { - height: auto; - z-index: 100; - position: absolute; - background: white; - padding: 5px; - position: fixed; - background: white; - border: black 1px solid; - .collectionSchema-header-toggler { - z-index: 100; - width: 100%; - height: 100%; - padding: 4px; - letter-spacing: 2px; - text-transform: uppercase; - svg { - margin-right: 4px; - } - } -} - -.collectionSchemaView-header { - height: 100%; - color: gray; - z-index: 100; - overflow-y: visible; - display: flex; - justify-content: space-between; - flex-wrap: wrap; -} - -button.add-column { - width: 28px; -} -.collectionSchemaView-menuOptions-wrapper { - background: rgb(241, 239, 235); - display: flex; +.collectionSchemaView { cursor: default; - height: 100%; - align-content: center; - align-items: center; -} - -.collectionSchema-header-menuOptions { - color: black; - width: 180px; - text-align: left; - .collectionSchema-headerMenu-group { - padding: 7px 0; - border-bottom: 1px solid lightgray; - cursor: pointer; - &:first-child { - padding-top: 0; - } - &:last-child { - border: none; - text-align: center; - padding: 12px 0 0 0; - } - } - label { - color: $medium-gray; - font-weight: normal; - letter-spacing: 2px; - text-transform: uppercase; - } - input { - color: black; - width: 100%; - } - .columnMenu-option { - cursor: pointer; - padding: 3px; - background-color: white; - transition: background-color 0.2s; - &:hover { - background-color: $light-gray; - } - &.active { - font-weight: bold; - border: 2px solid $light-gray; - } - svg { - color: gray; - margin-right: 5px; - width: 10px; - } - } - - .keys-dropdown { - position: relative; - //width: 100%; - background-color: white; - input { - border: 2px solid $light-gray; - padding: 3px; - height: 28px; - font-weight: bold; - letter-spacing: '2px'; - text-transform: 'uppercase'; - &:focus { - font-weight: normal; - } - } - } - .columnMenu-colors { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - .columnMenu-colorPicker { - cursor: pointer; - width: 20px; - height: 20px; - border-radius: 10px; - &.active { - border: 2px solid white; - box-shadow: 0 0 0 2px lightgray; - } - } - } -} -.schema-icon { - cursor: pointer; - width: 25px; - height: 25px; - display: flex; - align-items: center; - justify-content: center; - align-content: center; - background-color: $medium-blue; - color: white; - margin-right: 5px; - font-size: 10px; - border-radius: 3px; -} + .schema-table { + background-color: $white; -.keys-options-wrapper { - position: absolute; - text-align: left; - height: fit-content; - top: 100%; - z-index: 21; - background-color: #ffffff; - box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); - padding: 1px; - .key-option { - cursor: pointer; - color: #000000; - width: 100%; - height: 25px; - font-weight: 400; - display: flex; - justify-content: left; - align-items: center; - padding-left: 5px; - &:hover { - background-color: $light-gray; - } - } -} - -.collectionSchema-row { - height: 100%; - background-color: white; - &.row-focused .rt-td { - background-color: $light-blue; //$light-gray; - overflow: visible; - } - &.row-wrapped { - .rt-td { - white-space: normal; - } - } - .row-dragger { - display: flex; - justify-content: space-evenly; - width: 58px; - position: absolute; - /* max-width: 50px; */ - min-height: 30px; - align-items: center; - color: lightgray; - background-color: white; - transition: color 0.1s ease; - .row-option { - color: black; - cursor: pointer; - position: relative; - transition: color 0.1s ease; + .schema-header-row, + .schema-row { display: flex; - flex-direction: column; - justify-content: center; - z-index: 2; - border-radius: 3px; - padding: 3px; - &:hover { - background-color: $light-gray; + flex-direction: row; + + .schema-column-header, + .schema-table-cell, + .row-menu { + min-width: 50px; + border: 1px solid $medium-gray; + padding: 5px; + overflow: hidden; } } - } - .collectionSchema-row-wrapper { - &.row-above { - border-top: 1px solid $medium-blue; - } - &.row-below { - border-bottom: 1px solid $medium-blue; - } - &.row-inside { - border: 2px dashed $medium-blue; - } - .row-dragging { - background-color: blue; - } - } -} -.collectionSchemaView-cellContainer { - width: 100%; - height: unset; -} - -.collectionSchemaView-cellContents { - width: 100%; -} + .schema-header-row { + justify-content: flex-end; -.collectionSchemaView-cellWrapper { - display: flex; - height: 100%; - text-align: left; - padding-left: 19px; - position: relative; - align-items: center; - align-content: center; - &:focus { - outline: none; - } - &.editing { - padding: 0; - box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); - transform: scale(1.1); - z-index: 40; - input { - outline: 0; - border: none; - background-color: $white; - width: 100%; - height: fit-content; - min-height: 26px; - } - } - &.focused { - overflow: hidden; - &.inactive { - border: none; - } - } - p { - width: 100%; - height: 100%; - } - &:hover .collectionSchemaView-cellContents-docExpander { - display: block; - } - .collectionSchemaView-cellContents-document { - display: inline-block; - } - .collectionSchemaView-cellContents-docButton { - float: right; - width: '15px'; - height: '15px'; - } - .collectionSchemaView-dropdownWrapper { - border: grey; - border-style: solid; - border-width: 1px; - height: 30px; - .collectionSchemaView-dropdownButton { - //display: inline-block; - float: left; - height: 100%; - } - .collectionSchemaView-dropdownText { - display: inline-block; - //float: right; - height: 100%; - display: 'flex'; - font-size: 13; - justify-content: 'center'; - align-items: 'center'; - } - } - .collectionSchemaView-dropdownContainer { - position: absolute; - border: 1px solid rgba(0, 0, 0, 0.04); - box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14); - .collectionSchemaView-dropdownOption:hover { - background-color: rgba(0, 0, 0, 0.14); - cursor: pointer; + .schema-column-header { + font-weight: bold; + } } - } -} - -.collectionSchemaView-cellContents-docExpander { - height: 30px; - width: 30px; - display: none; - position: absolute; - top: 0; - right: 0; - background-color: lightgray; -} - -.doc-drag-over { - background-color: red; -} - -.collectionSchemaView-toolbar { - z-index: 100; -} - -.collectionSchemaView-toolbar { - height: 30px; - display: flex; - justify-content: flex-end; - padding: 0 10px; - border-bottom: 2px solid gray; - .collectionSchemaView-toolbar-item { - display: flex; - flex-direction: column; - justify-content: center; - } -} - -#preview-schema-checkbox-div { - margin-left: 20px; - font-size: 12px; -} -.collectionSchemaView-table { - width: 100%; - height: 100%; - overflow: auto; - padding: 3px; -} + .schema-row { + justify-content: space-evenly; -.rt-td.rt-expandable { - overflow: visible; - position: relative; - height: 100%; - z-index: 1; -} + .row-menu { + display: flex; + flex-direction: row; + justify-content: center; -.reactTable-sub { - background-color: rgb(252, 252, 252); - width: 100%; - .rt-thead { - display: none; - } - .row-dragger { - background-color: rgb(252, 252, 252); - } - .rt-table { - background-color: rgb(252, 252, 252); - } - .collectionSchemaView-table { - width: 100%; - border: solid 1px; - overflow: visible; - padding: 0px; - } -} + .row-button { + width: 20px; + height: 20px; + border-radius: 100%; + background-color: $dark-gray; + color: white; + padding: 5px; + cursor: pointer; + } + } -.collectionSchemaView-expander { - height: 100%; - min-height: 30px; - position: absolute; - color: gray; - width: 20; - height: auto; - left: 55; - svg { - position: absolute; - top: 50%; - left: 10; - transform: translate(-50%, -50%); + .row-cells { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + } } } - -.collectionSchemaView-addRow { - color: gray; - letter-spacing: 2px; - text-transform: uppercase; - cursor: pointer; - font-size: 10.5px; - margin-left: 50px; - margin-top: 10px; -} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index c4ee1805f..9ca1644e3 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,646 +1,164 @@ import React = require('react'); -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked } from 'mobx'; +import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx'; import { observer } from 'mobx-react'; -import Measure from 'react-measure'; -import { Resize } from 'react-table'; -import { Doc, Opt } from '../../../../fields/Doc'; +import { Doc } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; -import { PastelSchemaPalette, SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast, NumCast } from '../../../../fields/Types'; -import { TraceMobx } from '../../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; -import { DocUtils } from '../../../documents/Documents'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; -import { ContextMenu } from '../../ContextMenu'; -import { ContextMenuProps } from '../../ContextMenuItem'; -import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; -import { DocumentView } from '../../nodes/DocumentView'; -import { DefaultStyleProvider } from '../../StyleProvider'; +import { Cast } from '../../../../fields/Types'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; -import { SchemaTable } from './SchemaTable'; -// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 +import { SchemaColumnHeader } from './SchemaColumnHeader'; +import { SchemaRowBox } from './SchemaRowBox'; +import { DragManager } from '../../../util/DragManager'; +import { PresBox } from '../../nodes/trails/PresBox'; +import { UndoManager } from '../../../util/UndoManager'; +import { returnFalse } from '../../../../Utils'; export enum ColumnType { - Any, Number, String, Boolean, Doc, Image, - List, - Date, } -// this map should be used for keys that should have a const type of value -const columnTypes: Map<string, ColumnType> = new Map([ - ['title', ColumnType.String], - ['x', ColumnType.Number], - ['y', ColumnType.Number], - ['_width', ColumnType.Number], - ['_height', ColumnType.Number], - ['_nativeWidth', ColumnType.Number], - ['_nativeHeight', ColumnType.Number], - ['isPrototype', ColumnType.Boolean], - ['_curPage', ColumnType.Number], - ['_currentTimecode', ColumnType.Number], - ['zIndex', ColumnType.Number], -]); + +const defaultColumnKeys: string[] = ['title', 'type', 'author', 'text', 'data', 'tags']; @observer export class CollectionSchemaView extends CollectionSubView() { - private _previewCont?: HTMLDivElement; + private _lastSelectedRow: number | undefined; - @observable _previewDoc: Doc | undefined = undefined; - @observable _focusedTable: Doc = this.props.Document; - @observable _col: any = ''; - @observable _menuWidth = 0; - @observable _headerOpen = false; - @observable _headerIsEditing = false; - @observable _menuHeight = 0; - @observable _pointerX = 0; - @observable _pointerY = 0; - @observable _openTypes: boolean = false; + @observable _rowMenuWidth: number = 60; + @observable _selectedDocs: ObservableSet = new ObservableSet<SchemaRowBox>(); + @observable _isDragging: 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 - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); - } - @computed get borderWidth() { - return Number(COLLECTION_BORDER_WIDTH); - } - @computed get scale() { - return this.props.ScreenToLocalTransform().Scale; - } - @computed get columns() { - return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); - } - set columns(columns: SchemaHeaderField[]) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); + @computed get columnKeys() { + return Cast(this.props.Document.columnKeys, listSpec('string'), defaultColumnKeys); } - @computed get menuCoordinates() { - let searchx = 0; - let searchy = 0; - if (this.props.Document._searchDoc) { - const el = document.getElementsByClassName('collectionSchemaView-searchContainer')[0]; - if (el !== undefined) { - const rect = el.getBoundingClientRect(); - searchx = rect.x; - searchy = rect.y; - } - } - const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx; - const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy; - return this.props.ScreenToLocalTransform().transformPoint(x, y); - } - - get documentKeys() { - const docs = this.childDocs; - const 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.heading] = true)); - return Array.from(Object.keys(keys)); + @computed get columnWidths() { + return Cast( + this.props.Document.columnWidths, + listSpec('number'), + this.columnKeys.map(() => (this.props.PanelWidth() - this._rowMenuWidth) / this.columnKeys.length) + ); } - @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing); - - @undoBatch - setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => { - this._openTypes = false; - if (columnTypes.get(columnField.heading)) return; - - const columns = this.columns; - const index = columns.indexOf(columnField); - if (index > -1) { - columnField.setType(NumCast(type)); - columns[index] = columnField; - this.columns = columns; - } - }); - - @undoBatch - setColumnColor = (columnField: SchemaHeaderField, color: string): void => { - const columns = this.columns; - const index = columns.indexOf(columnField); - if (index > -1) { - columnField.setColor(color); - columns[index] = columnField; - this.columns = columns; // need to set the columns to trigger rerender - } - }; - - @undoBatch @action - setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { - const columns = this.columns; - columns.forEach(col => col.setDesc(undefined)); - - const index = columns.findIndex(c => c.heading === columnField.heading); - const column = columns[index]; - column.setDesc(descending); - columns[index] = column; - this.columns = columns; + changeColumnKey = (index: number, newKey: string) => { + let currKeys = this.columnKeys; + currKeys[index] = newKey; + this.layoutDoc.columnKeys = new List<string>(currKeys); + return true; }; - renderTypes = (col: any) => { - if (columnTypes.get(col.heading)) return null; - - const type = col.type; - - const anyType = ( - <div className={'columnMenu-option' + (type === ColumnType.Any ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Any)}> - <FontAwesomeIcon icon={'align-justify'} size="sm" /> - Any - </div> - ); - - const numType = ( - <div className={'columnMenu-option' + (type === ColumnType.Number ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Number)}> - <FontAwesomeIcon icon={'hashtag'} size="sm" /> - Number - </div> - ); - - const textType = ( - <div className={'columnMenu-option' + (type === ColumnType.String ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.String)}> - <FontAwesomeIcon icon={'font'} size="sm" /> - Text - </div> - ); - - const boolType = ( - <div className={'columnMenu-option' + (type === ColumnType.Boolean ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Boolean)}> - <FontAwesomeIcon icon={'check-square'} size="sm" /> - Checkbox - </div> - ); - - const listType = ( - <div className={'columnMenu-option' + (type === ColumnType.List ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.List)}> - <FontAwesomeIcon icon={'list-ul'} size="sm" /> - List - </div> - ); - - const docType = ( - <div className={'columnMenu-option' + (type === ColumnType.Doc ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Doc)}> - <FontAwesomeIcon icon={'file'} size="sm" /> - Document - </div> - ); - - const imageType = ( - <div className={'columnMenu-option' + (type === ColumnType.Image ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Image)}> - <FontAwesomeIcon icon={'image'} size="sm" /> - Image - </div> - ); - - const dateType = ( - <div className={'columnMenu-option' + (type === ColumnType.Date ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Date)}> - <FontAwesomeIcon icon={'calendar'} size="sm" /> - Date - </div> - ); - - const allColumnTypes = ( - <div className="columnMenu-types"> - {anyType} - {numType} - {textType} - {boolType} - {listType} - {docType} - {imageType} - {dateType} - </div> - ); - - const justColType = - type === ColumnType.Any - ? anyType - : type === ColumnType.Number - ? numType - : type === ColumnType.String - ? textType - : type === ColumnType.Boolean - ? boolType - : type === ColumnType.List - ? listType - : type === ColumnType.Doc - ? docType - : type === ColumnType.Date - ? dateType - : imageType; - - return ( - <div className="collectionSchema-headerMenu-group" onClick={action(() => (this._openTypes = !this._openTypes))}> - <div> - <label style={{ cursor: 'pointer' }}>Column type:</label> - <FontAwesomeIcon icon={'caret-down'} size="lg" style={{ float: 'right', transform: `rotate(${this._openTypes ? '180deg' : 0})`, transition: '0.2s all ease' }} /> - </div> - {this._openTypes ? allColumnTypes : justColType} - </div> - ); - }; - - renderSorting = (col: any) => { - const sort = col.desc; - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Sort by:</label> - <div className="columnMenu-sort"> - <div className={'columnMenu-option' + (sort === true ? ' active' : '')} onClick={() => this.setColumnSort(col, true)}> - <FontAwesomeIcon icon="sort-amount-down" size="sm" /> - Sort descending - </div> - <div className={'columnMenu-option' + (sort === false ? ' active' : '')} onClick={() => this.setColumnSort(col, false)}> - <FontAwesomeIcon icon="sort-amount-up" size="sm" /> - Sort ascending - </div> - <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}> - <FontAwesomeIcon icon="times" size="sm" /> - Clear sorting - </div> - </div> - </div> - ); - }; - - renderColors = (col: any) => { - const selected = col.color; - - const pink = PastelSchemaPalette.get('pink2'); - const purple = PastelSchemaPalette.get('purple2'); - const blue = PastelSchemaPalette.get('bluegreen1'); - const yellow = PastelSchemaPalette.get('yellow4'); - const red = PastelSchemaPalette.get('red2'); - const gray = '#f1efeb'; - - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Color:</label> - <div className="columnMenu-colors"> - <div className={'columnMenu-colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div> - <div className={'columnMenu-colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div> - <div className={'columnMenu-colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div> - <div className={'columnMenu-colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div> - <div className={'columnMenu-colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div> - <div className={'columnMenu-colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div> - </div> - </div> - ); - }; - - @undoBatch @action - changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, 'f1efeb')]); - } else { - if (addNew) { - columns.push(new SchemaHeaderField(newKey, 'f1efeb')); - this.columns = columns; + selectRow = (e: React.PointerEvent, doc: Doc) => { + const ctrl = e.ctrlKey || e.metaKey; + const shift = e.shiftKey; + if (shift && this._lastSelectedRow !== undefined) { + const currRowIndex = this.childDocs.indexOf(doc); + const startRow = Math.min(this._lastSelectedRow, currRowIndex); + const endRow = Math.max(this._lastSelectedRow, currRowIndex); + for (let i = startRow; i <= endRow; i++) { + const currDoc: Doc = this.childDocs[i]; + if (!this._selectedDocs.has(currDoc)) this._selectedDocs.add(currDoc); + } + this._lastSelectedRow = endRow; + } else if (ctrl) { + if (!this._selectedDocs.has(doc)) { + this._selectedDocs.add(doc); + this._lastSelectedRow = this.childDocs.indexOf(doc); } else { - const index = columns.map(c => c.heading).indexOf(oldKey); - if (index > -1) { - const column = columns[index]; - column.setHeading(newKey); - columns[index] = column; - this.columns = columns; - if (filter) { - Doc.setDocFilter(this.props.Document, newKey, filter, 'match'); - } else { - this.props.Document._docFilters = undefined; - } - } + this._selectedDocs.delete(doc); } - } - }; - - @action - openHeader = (col: any, screenx: number, screeny: number) => { - this._col = col; - this._headerOpen = true; - this._pointerX = screenx; - this._pointerY = screeny; - }; - - @action - closeHeader = () => { - this._headerOpen = false; - }; - - @undoBatch - @action - deleteColumn = (key: string) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([]); } else { - const index = columns.map(c => c.heading).indexOf(key); - if (index > -1) { - columns.splice(index, 1); - this.columns = columns; - } + this._selectedDocs.clear(); + this._selectedDocs.add(doc); + this._lastSelectedRow = this.childDocs.indexOf(doc); } - this.closeHeader(); - }; - - getPreviewTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(-this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth); }; @action - onHeaderClick = (e: React.PointerEvent) => { - e.stopPropagation(); - }; - - @action - onWheel(e: React.WheelEvent) { - const scale = this.props.ScreenToLocalTransform().Scale; - this.props.isContentActive(true) && e.stopPropagation(); - } - - @computed get renderMenuContent() { - TraceMobx(); - return ( - <div className="collectionSchema-header-menuOptions"> - {this.renderTypes(this._col)} - {this.renderColors(this._col)} - <div className="collectionSchema-headerMenu-group"> - <button - onClick={() => { - this.deleteColumn(this._col.heading); - }}> - Hide Column - </button> - </div> - </div> - ); - } - - private createTarget = (ele: HTMLDivElement) => { - this._previewCont = ele; - super.CreateDropTarget(ele); - }; - - isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable; - - @action setFocused = (doc: Doc) => (this._focusedTable = doc); - - @action setPreviewDoc = (doc: Opt<Doc>) => { - SelectionManager.SelectSchemaViewDoc(doc); - this._previewDoc = doc; - }; - - //toggles preview side-panel of schema - @action - toggleExpander = () => { - this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; - }; - - onDividerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander); - }; - @action - onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - const nativeWidth = this._previewCont!.getBoundingClientRect(); - const minWidth = 40; - const maxWidth = 1000; - const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; - const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; - this.props.Document.schemaPreviewWidth = width; - return false; + sortedSelectedDocs = (): Doc[] => { + return this.childDocs.filter(doc => this._selectedDocs.has(doc)); }; - onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { - if (this.props.isSelected(true)) e.stopPropagation(); - else this.props.select(false); + @action.bound + moveDocument = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => { + console.log('hello'); + console.log(targetCollection?.title); + if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { + console.log('hi'); + return true; } - }; - - @computed - get previewDocument(): Doc | undefined { - return this._previewDoc; - } - - @computed - get dividerDragger() { - return this.previewWidth() === 0 ? null : ( - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown}> - <div className="collectionSchemaView-dividerDragger" /> - </div> - ); - } - - @computed - get previewPanel() { - return ( - <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}> - {!this.previewDocument ? null : ( - <DocumentView - Document={this.previewDocument} - DataDoc={undefined} - fitContentsToBox={returnTrue} - dontCenter={'y'} - focus={DocUtils.DefaultFocus} - renderDepth={this.props.renderDepth} - rootSelected={this.rootSelected} - PanelWidth={this.previewWidth} - PanelHeight={this.previewHeight} - isContentActive={returnTrue} - isDocumentActive={returnFalse} - ScreenToLocalTransform={this.getPreviewTransform} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - styleProvider={DefaultStyleProvider} - docViewPath={returnEmptyDoclist} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - moveDocument={this.props.moveDocument} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - /> - )} - </div> - ); - } - - @computed - get schemaTable() { - return ( - <SchemaTable - Document={this.props.Document} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.props.PanelWidth} - childDocs={this.childDocs} - CollectionView={this.props.CollectionView} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - fieldKey={this.props.fieldKey} - renderDepth={this.props.renderDepth} - moveDocument={this.props.moveDocument} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - active={this.props.isContentActive} - onDrop={this.onExternalDrop} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - isSelected={this.props.isSelected} - isFocused={this.isFocused} - setFocused={this.setFocused} - setPreviewDoc={this.setPreviewDoc} - deleteDocument={this.props.removeDocument} - addDocument={this.props.addDocument} - dataDoc={this.props.DataDoc} - columns={this.columns} - documentKeys={this.documentKeys} - headerIsEditing={this._headerIsEditing} - openHeader={this.openHeader} - onClick={this.onTableClick} - onPointerDown={emptyFunction} - onResizedChange={this.onResizedChange} - setColumns={this.setColumns} - reorderColumns={this.reorderColumns} - changeColumns={this.changeColumns} - setHeaderIsEditing={this.setHeaderIsEditing} - changeColumnSort={this.setColumnSort} - /> - ); - } - - @computed - public get schemaToolbar() { - return ( - <div className="collectionSchemaView-toolbar"> - <div className="collectionSchemaView-toolbar-item"> - <div id="preview-schema-checkbox-div"> - <input type="checkbox" key={'Show Preview'} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> - Show Preview - </div> - </div> - </div> - ); - } - - onSpecificMenu = (e: React.MouseEvent) => { - if ((e.target as any)?.className?.includes?.('collectionSchemaView-cell') || e.target instanceof HTMLSpanElement) { - const cm = ContextMenu.Instance; - const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - optionItems.push({ description: 'remove', event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: 'trash' }); - !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); - cm.displayMenu(e.clientX, e.clientY); - (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. - e.stopPropagation(); + const first = document instanceof Doc ? document : document[0]; + if (!first?._stayInCollection && addDocument !== returnFalse) { + UndoManager.RunInTempBatch(() => this.props.removeDocument?.(document) && addDocument(document)); + return true; } + return false; }; @action - onTableClick = (e: React.MouseEvent): void => { - if (!(e.target as any)?.className?.includes?.('collectionSchemaView-cell') && !(e.target instanceof HTMLSpanElement)) { - this.setPreviewDoc(undefined); - } else { - e.stopPropagation(); - } - this.setFocused(this.props.Document); - this.closeHeader(); - }; - - onResizedChange = (newResized: Resize[], event: any) => { - const columns = this.columns; - newResized.forEach(resized => { - const index = columns.findIndex(c => c.heading === resized.id); - const column = columns[index]; - column.setWidth(resized.value); - columns[index] = column; - }); - this.columns = columns; + startDrag = (e: React.PointerEvent, doc: Doc) => { + if (this._selectedDocs.size === 0) this._selectedDocs.add(doc); + this._isDragging = true; + const dragData = new DragManager.DocumentDragData(this.sortedSelectedDocs(), 'move'); + dragData.moveDocument = this.moveDocument; + const dragItem: HTMLElement[] = []; + const dragDiv = document.createElement('div'); + dragDiv.className = 'presItem-multiDrag'; + dragDiv.innerText = 'Move ' + this._selectedDocs.size + ' rows'; + dragDiv.style.position = 'absolute'; + dragDiv.style.top = e.clientY + 'px'; + dragDiv.style.left = e.clientX - 50 + 'px'; + dragItem.push(dragDiv); + + DragManager.StartDocumentDrag( + dragItem.map(ele => ele), + dragData, + e.clientX, + e.clientY, + undefined + ); + this._isDragging = false; + return true; }; @action - setColumns = (columns: SchemaHeaderField[]) => (this.columns = columns); - - @undoBatch - reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => { - const columns = [...columnsValues]; - const oldIndex = columns.indexOf(toMove); - const relIndex = columns.indexOf(relativeTo); - const 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.columns = columns; + endDrag = (e: React.PointerEvent) => { + // this._isDragging = false; }; - onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation(); - render() { - TraceMobx(); - if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0); - const menuContent = this.renderMenuContent; - const menu = ( - <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ transform: `translate(${this.menuCoordinates[0]}px, ${this.menuCoordinates[1]}px)` }}> - <Measure - offset - onResize={action((r: any) => { - const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); - this._menuWidth = dim[0]; - this._menuHeight = dim[1]; - })}> - {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} - </Measure> - </div> - ); return ( - <div - className={'collectionSchemaView' + (this.props.Document._searchDoc ? '-searchContainer' : '-container')} - style={{ - overflow: this.props.scrollOverflow === true ? 'scroll' : undefined, - backgroundColor: 'white', - pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined, - width: this.props.PanelWidth() || '100%', - height: this.props.PanelHeight() || '100%', - position: 'relative', - }}> - <div - className="collectionSchemaView-tableContainer" - style={{ width: `calc(100% - ${this.previewWidth()}px)` }} - onContextMenu={this.onSpecificMenu} - onPointerDown={this.onPointerDown} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} - onDrop={e => this.onExternalDrop(e, {})} - ref={this.createTarget}> - {this.schemaTable} + <div className="collectionSchemaView"> + <div className="schema-table"> + <div className="schema-header-row"> + {this.columnKeys.map((key, index) => ( + <SchemaColumnHeader columnIndex={index} columnKeys={this.columnKeys} columnWidths={this.columnWidths} changeColumnKey={this.changeColumnKey} /> + ))} + </div> + <div className="schema-table-content"> + {this.childDocs.map((doc: Doc) => ( + <SchemaRowBox + {...this.props} + Document={doc} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + columnKeys={this.columnKeys} + columnWidths={this.columnWidths} + rowMenuWidth={this._rowMenuWidth} + selectedRows={this._selectedDocs} + selectRow={this.selectRow} + startDrag={this.startDrag} + endDrag={this.endDrag} + dragging={this._isDragging} + /> + ))} + </div> </div> - {this.dividerDragger} - {!this.previewWidth() ? null : this.previewPanel} - {this._headerOpen && this.props.isContentActive() ? menu : null} </div> ); } diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx new file mode 100644 index 000000000..9bd1b843f --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -0,0 +1,28 @@ +import React = require('react'); +import { observer } from 'mobx-react'; +import './CollectionSchemaView.scss'; +import { EditableView } from '../../EditableView'; +import { emptyFunction } from '../../../../Utils'; +import { action, computed } from 'mobx'; + +export interface SchemaColumnHeaderProps { + columnKeys: string[]; + columnWidths: number[]; + columnIndex: number; + changeColumnKey: (index: number, newKey: string) => boolean; +} + +@observer +export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> { + @computed get fieldKey() { + return this.props.columnKeys[this.props.columnIndex]; + } + + render() { + return ( + <div className="schema-column-header" style={{ width: this.props.columnWidths[this.props.columnIndex] }}> + <EditableView SetValue={(newKey: string) => this.props.changeColumnKey(this.props.columnIndex, newKey)} GetValue={() => this.fieldKey} contents={this.fieldKey} /> + </div> + ); + } +} diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx new file mode 100644 index 000000000..0065a0938 --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -0,0 +1,97 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, ObservableMap, ObservableSet } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { undoBatch } from '../../../util/UndoManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { Colors } from '../../global/globalEnums'; +import { FieldViewProps } from '../../nodes/FieldView'; +import './CollectionSchemaView.scss'; +import { SchemaTableCell } from './SchemaTableCell'; +import { emptyFunction, setupMoveUpEvents } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; + +export interface SchemaRowBoxProps extends FieldViewProps { + columnKeys: string[]; + columnWidths: number[]; + rowMenuWidth: number; + selectedRows: ObservableSet<Doc>; + selectRow: (e: any, doc: Doc) => void; + startDrag: (e: any, doc: Doc) => boolean; + endDrag: (e: any) => void + dragging: boolean; +} + +@observer +export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { + private _ref: HTMLDivElement | null = null; + + isSelected = () => this.props.selectedRows.has(this.props.Document); + bounds = () => this._ref?.getBoundingClientRect(); + + @action + onRowPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + + setupMoveUpEvents( + this, e, + (e) => this.props.startDrag(e, this.props.Document), + emptyFunction, + (e) => this.props.selectRow(e, this.props.Document) + ) + } + + onPointerEnter = (e: any) => { + document.removeEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointermove', this.onPointerMove); + }; + + onPointerMove = (e: any) => { + let dragIsRow: boolean = true; + DragManager.docsBeingDragged.forEach(doc => { + dragIsRow = this.props.selectedRows.has(doc); + }) + if (this._ref && dragIsRow) { + const rect = this._ref.getBoundingClientRect(); + const y = e.clientY - rect.top; //y position within the element. + const height = this._ref.clientHeight; + const halfLine = height / 2; + if (y <= halfLine) { + this._ref.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; + this._ref.style.borderBottom = '0px'; + } else if (y > halfLine) { + this._ref.style.borderTop = '0px'; + this._ref.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; + } + } + }; + + onPointerLeave = (e: any) => { + if (this._ref) { + this._ref.style.borderTop = '0px'; + this._ref.style.borderBottom = '0px'; + } + document.removeEventListener('pointermove', this.onPointerMove); + }; + + render() { + return ( + <div className="schema-row" style={this.isSelected() ? { backgroundColor: Colors.LIGHT_BLUE, opacity: this.props.dragging ? 0.5 : 1} : {}} onPointerDown={this.onRowPointerDown} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} ref={(row: HTMLDivElement | null) => (this._ref = row)}> + <div className="row-menu" style={{width: this.props.rowMenuWidth}}> + <div className="row-button" onPointerDown={undoBatch((e) => {e.stopPropagation(); this.props.removeDocument?.(this.props.Document)})}> + <FontAwesomeIcon icon="times" /> + </div> + <div className="row-button" onPointerDown={(e) => {e.stopPropagation(); this.props.addDocTab(this.props.Document, 'add:right')}}> + <FontAwesomeIcon icon="external-link-alt" /> + </div> + </div> + <div className="row-cells"> + {this.props.columnKeys.map((key, index) => ( + <SchemaTableCell Document={this.props.Document} key={key} fieldKey={key} columnWidth={this.props.columnWidths[index]} /> + ))} + </div> + </div> + ); + } +} diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx new file mode 100644 index 000000000..1eb5e0e01 --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -0,0 +1,24 @@ +import React = require('react'); +import { observer } from 'mobx-react'; +import { Doc, Field } from '../../../../fields/Doc'; +import { StrCast } from '../../../../fields/Types'; +import './CollectionSchemaView.scss'; + +export interface SchemaTableCellProps { + Document: Doc; + fieldKey: string; + columnWidth: number; +} + +@observer +export class SchemaTableCell extends React.Component<SchemaTableCellProps> { + render() { + return ( + <div className="schema-table-cell" style={{ width: this.props.columnWidth }}> + {/* {StrCast(this.props.Document[this.props.fieldKey])} */} + {/* Field.toKeyValueString(this.props.Document, this.props.fieldKey) */} + {Field.toString(this.props.Document[this.props.fieldKey] as Field)} + </div> + ); + } +} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaCells.tsx index adcd9e1e3..fb93d8b8e 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaCells.tsx @@ -1,35 +1,35 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { extname } from "path"; -import DatePicker from "react-datepicker"; -import { CellInfo } from "react-table"; -import { DateField } from "../../../../fields/DateField"; -import { Doc, DocListCast, Field, Opt } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, DateCast, FieldValue, StrCast } from "../../../../fields/Types"; -import { ImageField } from "../../../../fields/URLField"; -import { emptyFunction, Utils } from "../../../../Utils"; -import { Docs } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { DragManager } from "../../../util/DragManager"; -import { KeyCodes } from "../../../util/KeyCodes"; -import { CompileScript } from "../../../util/Scripting"; -import { SearchUtil } from "../../../util/SearchUtil"; -import { SnappingManager } from "../../../util/SnappingManager"; -import { undoBatch } from "../../../util/UndoManager"; +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { extname } from 'path'; +import DatePicker from 'react-datepicker'; +import { CellInfo } from 'react-table'; +import { DateField } from '../../../../fields/DateField'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; +import { ComputedField } from '../../../../fields/ScriptField'; +import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { emptyFunction, Utils } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { DragManager } from '../../../util/DragManager'; +import { KeyCodes } from '../../../util/KeyCodes'; +import { CompileScript } from '../../../util/Scripting'; +import { SearchUtil } from '../../../util/SearchUtil'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { undoBatch } from '../../../util/UndoManager'; import '../../../views/DocumentDecorations.scss'; -import { EditableView } from "../../EditableView"; +import { EditableView } from '../../EditableView'; import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss'; -import { DocumentIconContainer } from "../../nodes/DocumentIcon"; -import { OverlayView } from "../../OverlayView"; -import { CollectionView } from "../CollectionView"; -import "./CollectionSchemaView.scss"; +import { DocumentIconContainer } from '../../nodes/DocumentIcon'; +import { OverlayView } from '../../OverlayView'; +import { CollectionView } from '../CollectionView'; +import './CollectionSchemaView.scss'; // intialize cell properties export interface CellProps { @@ -48,8 +48,7 @@ export interface CellProps { // called when a button is pressed on the node itself addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; - moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, - addDocument: (document: Doc | Doc[]) => boolean) => boolean; + moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; // set whether the cell is in the isEditing mode @@ -67,7 +66,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // return a field key that is corrected for whether it COMMENT public static resolvedFieldKey(column: string, rowDoc: Doc) { const fieldKey = column; - if (fieldKey.startsWith("*")) { + if (fieldKey.startsWith('*')) { const rootKey = fieldKey.substring(1); const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))]; const matchedKeys = allKeys.filter(key => key.includes(rootKey)); @@ -82,33 +81,37 @@ export class CollectionSchemaCell extends React.Component<CellProps> { protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); // methods for dragging and dropping protected _dropDisposer?: DragManager.DragDropDisposer; - @observable contents: string = ""; + @observable contents: string = ''; - componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } - componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } + componentDidMount() { + document.addEventListener('keydown', this.onKeyDown); + } + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeyDown); + } @action onKeyDown = (e: KeyboardEvent): void => { // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) { - document.removeEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); this._isEditing = true; this.props.setIsEditing(true); } - } + }; @action isEditingCallback = (isEditing: boolean): void => { // a general method that takes a boolean that determines whether the cell should be in // is-editing mode // remove the event listener if it's there - document.removeEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); // it's not already in is-editing mode, re-add the event listener - isEditing && document.addEventListener("keydown", this.onKeyDown); + isEditing && document.addEventListener('keydown', this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - } + }; @action onPointerDown = async (e: React.PointerEvent): Promise<void> => { @@ -119,19 +122,19 @@ export class CollectionSchemaCell extends React.Component<CellProps> { this.props.setPreviewDoc(this.props.rowProps.original); let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { + if ((url = StrCast(this.props.rowProps.row.href))) { // opens up the the doc in a new window, blurring the old one try { new URL(url); const temp = window.open(url)!; temp.blur(); window.focus(); - } catch { } + } catch {} } const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null); doc && this.props.setPreviewDoc(doc); - } + }; @undoBatch applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => { @@ -142,7 +145,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { doc[this.renderFieldKey] = res.result; return true; // return whether the change was successful - } + }; private drop = (e: Event, de: DragManager.DropEvent) => { // if the drag has data at its completion @@ -151,41 +154,51 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (de.complete.docDragData.draggedDocuments.length === 1) { // update the renderFieldKey this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; - } - else { + } else { // create schema document reflecting the new column arrangement - const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); + const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {}); this._rowDataDoc[this.renderFieldKey] = coll; } e.stopPropagation(); } - } + }; protected dropRef = (ele: HTMLElement | null) => { // if the drop disposer is not undefined, run its function this._dropDisposer?.(); // if ele is not null, give ele a non-undefined drop disposer ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); - } + }; returnHighlights(contents: string, positions?: number[]) { if (positions) { const results = []; StrCast(this.props.Document._searchString); const length = StrCast(this.props.Document._searchString).length; - const color = contents ? "black" : "grey"; + const color = contents ? 'black' : 'grey'; - results.push(<span key="-1" style={{ color }}>{contents?.slice(0, positions[0])}</span>); + results.push( + <span key="-1" style={{ color }}> + {contents?.slice(0, positions[0])} + </span> + ); positions.forEach((num, cur) => { - results.push(<span key={"start" + cur} style={{ backgroundColor: "#FFFF00", color }}>{contents?.slice(num, num + length)}</span>); + results.push( + <span key={'start' + cur} style={{ backgroundColor: '#FFFF00', color }}> + {contents?.slice(num, num + length)} + </span> + ); let end = 0; - cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; - results.push(<span key={"end" + cur} style={{ color }}>{contents?.slice(num + length, end)}</span>); - } - ); + cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]); + results.push( + <span key={'end' + cur} style={{ color }}> + {contents?.slice(num + length, end)} + </span> + ); + }); return results; } - return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; + return <span style={{ color: contents ? 'black' : 'grey' }}>{contents ? contents?.valueOf() : 'undefined'}</span>; } @computed get renderFieldKey() { @@ -199,10 +212,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); // Jump to the this document - DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], - undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); } - } + }; renderCellWithType(type: string | undefined) { const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -214,29 +226,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const onPointerEnter = (e: React.PointerEvent): void => { // e.buttons === 1 means the left moue pointer is down - if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { - dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; + if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) { + dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over'; } }; const onPointerLeave = (e: React.PointerEvent): void => { // change the class name to indicate that the cell is no longer being dragged - dragRef.current!.className = "collectionSchemaView-cellContainer"; + dragRef.current!.className = 'collectionSchemaView-cellContainer'; }; let contents = Field.toString(field as Field); // display 2 hyphens instead of a blank box for empty cells - contents = contents === "" ? "--" : contents; + contents = contents === '' ? '--' : contents; // classname reflects the tatus of the cell - let className = "collectionSchemaView-cellWrapper"; - if (this._isEditing) className += " editing"; - if (this.props.isFocused && this.props.isEditable) className += " focused"; - if (this.props.isFocused && !this.props.isEditable) className += " inactive"; + let className = 'collectionSchemaView-cellWrapper'; + if (this._isEditing) className += ' editing'; + if (this.props.isFocused && this.props.isEditable) className += ' focused'; + if (this.props.isFocused && !this.props.isEditable) className += ' inactive'; const positions = []; - if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { + if (StrCast(this.props.Document._searchString).toLowerCase() !== '') { // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents - let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase(); + let term = field instanceof Promise ? '...promise pending...' : contents.toLowerCase(); const search = StrCast(this.props.Document._searchString).toLowerCase(); let start = term.indexOf(search); let tally = 0; @@ -256,56 +268,60 @@ export class CollectionSchemaCell extends React.Component<CellProps> { positions.pop(); } } - const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; + const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined'; return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }} - ref={dragRef} onPointerDown={this.onPointerDown} onClick={action(e => this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> + <div + className="collectionSchemaView-cellContainer" + style={{ cursor: field instanceof Doc ? 'grab' : 'auto' }} + ref={dragRef} + onPointerDown={this.onPointerDown} + onClick={action(e => (this._isEditing = true))} + onPointerEnter={onPointerEnter} + onPointerLeave={onPointerLeave}> <div className={className} ref={this._focusRef} tabIndex={-1}> - <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}> - {!this.props.Document._searchDoc ? + <div className="collectionSchemaView-cellContents" ref={type === undefined || type === 'document' ? this.dropRef : null}> + {!this.props.Document._searchDoc ? ( <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} - display={"inline"} + display={'inline'} contents={contents} - height={"auto"} + height={'auto'} maxHeight={Number(MAX_ROW_HEIGHT)} placeholder={placeholder} GetValue={() => { const cfield = ComputedField.WithoutComputed(() => FieldValue(field)); const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; - const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; - return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : - Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; + const cfinalScript = cscript?.split('return')[cscript.split('return').length - 1]; + return cscript ? (cfinalScript?.endsWith(';') ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : ''; }} SetValue={action((value: string) => { // sets what is displayed after the user makes an input let retVal = false; - if (value.startsWith(":=") || value.startsWith("=:=")) { + if (value.startsWith(':=') || value.startsWith('=:=')) { // decides how to compute a value when given either of the above strings - const script = value.substring(value.startsWith("=:=") ? 3 : 2); - retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); + const script = value.substring(value.startsWith('=:=') ? 3 : 2); + retVal = this.props.setComputed(script, value.startsWith(':=') ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); } else { // check if the input is a number let inputIsNum = true; for (const s of value) { - if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) { + if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) { inputIsNum = false; } } // check if the input is a boolean - const inputIsBool: boolean = value === "false" || value === "true"; - // what to do in the case - if (!inputIsNum && !inputIsBool && !value.startsWith("=")) { + const inputIsBool: boolean = value === 'false' || value === 'true'; + // what to do in the case + if (!inputIsNum && !inputIsBool && !value.startsWith('=')) { // if it's not a number, it's a string, and should be processed as such - // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically + // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically // after each edit let valueSansQuotes = value; if (this._isEditing) { const vsqLength = valueSansQuotes.length; // get rid of outer quotes - valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0, - valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength); + valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength); } let inputAsString = '"'; // escape any quotes in the string @@ -319,27 +335,27 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // add a closing quote inputAsString += '"'; //two options here: we can strip off outer quotes or we can figure out what's going on with the script - const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length; // change it if a change is made, otherwise, just compile using the old cell conetnts script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle numbers and expressions - } else if (inputIsNum || value.startsWith("=")) { + } else if (inputIsNum || value.startsWith('=')) { //TODO: make accept numbers - const inputscript = value.substring(value.startsWith("=") ? 1 : 0); + const inputscript = value.substring(value.startsWith('=') ? 1 : 0); // if commas are not stripped, the parser only considers the numbers after the last comma - let inputSansCommas = ""; + let inputSansCommas = ''; for (const s of inputscript) { - if (!(s === ",")) { + if (!(s === ',')) { inputSansCommas += s; } } - const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle booleans } else if (inputIsBool) { - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } @@ -352,33 +368,47 @@ export class CollectionSchemaCell extends React.Component<CellProps> { })} OnFillDown={async (value: string) => { // computes all of the value preceded by := - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - script.compiled && DocListCast(this.props.Document[this.props.fieldKey]). - forEach((doc, i) => value.startsWith(":=") ? - this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : - this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); + script.compiled && + DocListCast(this.props.Document[this.props.fieldKey]).forEach((doc, i) => + value.startsWith(':=') ? this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run) + ); }} /> - : + ) : ( this.returnHighlights(contents, positions) - } - </div > + )} + </div> </div> </div> ); } - render() { return this.renderCellWithType(undefined); } + render() { + return this.renderCellWithType(undefined); + } } @observer -export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } +export class CollectionSchemaNumberCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType('number'); + } +} @observer -export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType('boolean'); + } +} @observer -export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } +export class CollectionSchemaStringCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType('string'); + } +} @observer export class CollectionSchemaDateCell extends CollectionSchemaCell { @@ -396,24 +426,24 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell { // ^ DateCast is always undefined for some reason, but that is what the field should be set to this._rowDoc[this.renderFieldKey] = new DateField(date as Date); //} - } + }; render() { - return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> : - <DatePicker - selected={this._date?.date || new Date} - onSelect={date => this.handleChange(date)} - onChange={date => this.handleChange(date)} - />; + return !this.props.isFocused ? ( + <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : '--'}</span> + ) : ( + <DatePicker selected={this._date?.date || new Date()} onSelect={date => this.handleChange(date)} onChange={date => this.handleChange(date)} /> + ); } } @observer export class CollectionSchemaDocCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); } + @computed get _doc() { + return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); + } @action onSetValue = (value: string) => { @@ -422,7 +452,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const script = CompileScript(value, { addReturn: true, typecheck: true, - transformer: DocumentIconContainer.getTransformer() + transformer: DocumentIconContainer.getTransformer(), }); // compile the script const results = script.compiled && script.run(); @@ -432,44 +462,43 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { return true; } return false; - } + }; - componentWillUnmount() { this.onBlur(); } + componentWillUnmount() { + this.onBlur(); + } - onBlur = () => { this._overlayDisposer?.(); }; + onBlur = () => { + this._overlayDisposer?.(); + }; onFocus = () => { this.onBlur(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; @action isEditingCallback = (isEditing: boolean): void => { // the isEditingCallback from a general CollectionSchemaCell - document.removeEventListener("keydown", this.onKeyDown); - isEditing && document.addEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); + isEditing && document.addEventListener('keydown', this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - } + }; render() { // if there's a doc, render it - return !this._doc ? this.renderCellWithType("document") : - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} - onPointerDown={this.onPointerDown} - > - <div className="collectionSchemaView-cellContents-document" - style={{ padding: "5.9px" }} - ref={this.dropRef} - onFocus={this.onFocus} - onBlur={this.onBlur} - > + return !this._doc ? ( + this.renderCellWithType('document') + ) : ( + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents-document" style={{ padding: '5.9px' }} ref={this.dropRef} onFocus={this.onFocus} onBlur={this.onBlur}> <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} - display={"inline"} - contents={this._doc.title || "--"} - height={"auto"} + display={'inline'} + contents={this._doc.title || '--'} + height={'auto'} maxHeight={Number(MAX_ROW_HEIGHT)} GetValue={() => StrCast(this._doc?.title)} SetValue={action((value: string) => { @@ -477,33 +506,36 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { return true; })} /> - </div > - <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> + </div> + <div onClick={() => this._doc && this.props.addDocTab(this._doc, 'add:right')} className="collectionSchemaView-cellContents-docButton"> <FontAwesomeIcon icon="external-link-alt" size="lg" /> </div> - </div>; + </div> + ); } } @observer export class CollectionSchemaImageCell extends CollectionSchemaCell { - choosePath(url: URL) { - if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href + if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question const ext = extname(url.href); - return url.href.replace(ext, "_o" + ext); + return url.href.replace(ext, '_o' + ext); } render() { const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const alts = DocListCast(this._rowDoc[this.renderFieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images + const altpaths = alts + .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) + .filter(url => url) + .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; // If there is a path, follow it; otherwise, follow a link to a default image icon - const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px @@ -511,25 +543,28 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell { width = height * aspect; // increase the width of the image if necessary to maintain proportionality const reference = React.createRef<HTMLDivElement>(); - return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> - <img src={url[0]} - width={paths.length ? width : "20px"} - height={paths.length ? height : "20px"} /> - </div > - </div>; + return ( + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> + <img src={url[0]} width={paths.length ? width : '20px'} height={paths.length ? height : '20px'} /> + </div> + </div> + ); } } - @observer export class CollectionSchemaListCell extends CollectionSchemaCell { _overlayDisposer?: () => void; - @computed get _field() { return this._rowDoc[this.renderFieldKey]; } - @computed get _optionsList() { return this._field as List<any>; } + @computed get _field() { + return this._rowDoc[this.renderFieldKey]; + } + @computed get _optionsList() { + return this._field as List<any>; + } @observable private _opened = false; // whether the list is opened - @observable private _text = "select an item"; + @observable private _text = 'select an item'; @observable private _selectedNum = 0; // the index of the list item selected @action @@ -538,102 +573,109 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { this._optionsList[this._selectedNum] = this._text = value; (this._field as List<any>).splice(this._selectedNum, 1, value); - } + }; @action onSelected = (element: string, index: number) => { // if an item is selected, the private variables should update to reflect this this._text = element; this._selectedNum = index; - } + }; onFocus = () => { this._overlayDisposer?.(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; render() { const link = false; const reference = React.createRef<HTMLDivElement>(); - // if the list is not opened, don't display it; otherwise, do. + // if the list is not opened, don't display it; otherwise, do. if (this._optionsList?.length) { - const options = !this._opened ? (null) : + const options = !this._opened ? null : ( <div> {this._optionsList.map((element, index) => { const val = Field.toString(element); - return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} > - {val} - </div>; + return ( + <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: '6px' }} onPointerDown={e => this.onSelected(StrCast(element), index)}> + {val} + </div> + ); })} - </div>; - - const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>; - const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} > - <EditableView - editing={this._isEditing} - isEditingCallback={this.isEditingCallback} - display={"inline"} - contents={this._text} - height={"auto"} - maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => this._text} - SetValue={action((value: string) => { - // add special for params - this.onSetValue(value); - return true; - })} - /> - </div >; + </div> + ); + + const plainText = <div style={{ padding: '5.9px' }}>{this._text}</div>; + const textarea = ( + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: '5.9px' }} ref={this.dropRef}> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={'inline'} + contents={this._text} + height={'auto'} + maxHeight={Number(MAX_ROW_HEIGHT)} + GetValue={() => this._text} + SetValue={action((value: string) => { + // add special for params + this.onSetValue(value); + return true; + })} + /> + </div> + ); //☰ return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> <div className="collectionSchemaView-dropDownWrapper"> - <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }} - onClick={action(e => this._opened = !this._opened)} > - <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" /> + <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: 'length', position: 'relative' }} onClick={action(e => (this._opened = !this._opened))}> + <FontAwesomeIcon icon={this._opened ? 'caret-up' : 'caret-down'} size="sm" /> </button> <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div> </div> {options} - </div > + </div> </div> ); } - return this.renderCellWithType("list"); + return this.renderCellWithType('list'); } } - @observer export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } + @computed get _isChecked() { + return BoolCast(this._rowDoc[this.renderFieldKey]); + } render() { const reference = React.createRef<HTMLDivElement>(); return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} /> + <input type="checkbox" checked={this._isChecked} onChange={e => (this._rowDoc[this.renderFieldKey] = e.target.checked)} /> </div> ); } } - @observer export class CollectionSchemaButtons extends CollectionSchemaCell { // the navigation buttons for schema view when it is used for search. render() { - return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> : - <div style={{ paddingTop: 8, paddingLeft: 3 }} > + return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? ( + <></> + ) : ( + <div style={{ paddingTop: 8, paddingLeft: 3 }}> <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}> <FontAwesomeIcon icon="arrow-up" size="sm" /> </button> - <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} > + <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)}> <FontAwesomeIcon icon="arrow-down" size="sm" /> </button> - </div>; + </div> + ); } } diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx new file mode 100644 index 000000000..32283d76c --- /dev/null +++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx @@ -0,0 +1,510 @@ +// import React = require("react"); +// import { IconProp } from "@fortawesome/fontawesome-svg-core"; +// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +// import { action, computed, observable, runInAction, trace } from "mobx"; +// import { observer } from "mobx-react"; +// import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc"; +// import { listSpec } from "../../../../fields/Schema"; +// import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +// import { ScriptField } from "../../../../fields/ScriptField"; +// import { Cast, StrCast } from "../../../../fields/Types"; +// import { undoBatch } from "../../../util/UndoManager"; +// import { CollectionView } from "../CollectionView"; +// import { ColumnType } from "./CollectionSchemaView"; +// import "./CollectionSchemaView.scss"; + +// const higflyout = require("@hig/flyout"); +// export const { anchorPoints } = higflyout; +// export const Flyout = higflyout.default; + +// export interface AddColumnHeaderProps { +// createColumn: () => void; +// } + +// @observer +// export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> { +// // the button that allows the user to add a column +// render() { +// return <button className="add-column" onClick={() => this.props.createColumn()}> +// <FontAwesomeIcon icon="plus" size="sm" /> +// </button>; +// } +// } + +// export interface ColumnMenuProps { +// columnField: SchemaHeaderField; +// // keyValue: string; +// possibleKeys: string[]; +// existingKeys: string[]; +// // keyType: ColumnType; +// typeConst: boolean; +// menuButtonContent: JSX.Element; +// addNew: boolean; +// onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; +// setIsEditing: (isEditing: boolean) => void; +// deleteColumn: (column: string) => void; +// onlyShowOptions: boolean; +// setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; +// setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; +// anchorPoint?: any; +// setColumnColor: (column: SchemaHeaderField, color: string) => void; +// } +// @observer +// export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> { +// @observable private _isOpen: boolean = false; +// @observable private _node: HTMLDivElement | null = null; + +// componentDidMount() { document.addEventListener("pointerdown", this.detectClick); } + +// componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); } + +// @action +// detectClick = (e: PointerEvent) => { +// !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false); +// } + +// @action +// toggleIsOpen = (): void => { +// this.props.setIsEditing(this._isOpen = !this._isOpen); +// } + +// changeColumnType = (type: ColumnType) => { +// this.props.setColumnType(this.props.columnField, type); +// } + +// changeColumnSort = (desc: boolean | undefined) => { +// this.props.setColumnSort(this.props.columnField, desc); +// } + +// changeColumnColor = (color: string) => { +// this.props.setColumnColor(this.props.columnField, color); +// } + +// @action +// setNode = (node: HTMLDivElement): void => { +// if (node) { +// this._node = node; +// } +// } + +// renderTypes = () => { +// if (this.props.typeConst) return (null); + +// const type = this.props.columnField.type; +// return ( +// <div className="collectionSchema-headerMenu-group"> +// <label>Column type:</label> +// <div className="columnMenu-types"> +// <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}> +// <FontAwesomeIcon icon={"align-justify"} size="sm" /> +// Any +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}> +// <FontAwesomeIcon icon={"hashtag"} size="sm" /> +// Number +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}> +// <FontAwesomeIcon icon={"font"} size="sm" /> +// Text +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}> +// <FontAwesomeIcon icon={"check-square"} size="sm" /> +// Checkbox +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List)}> +// <FontAwesomeIcon icon={"list-ul"} size="sm" /> +// List +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}> +// <FontAwesomeIcon icon={"file"} size="sm" /> +// Document +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image)}> +// <FontAwesomeIcon icon={"image"} size="sm" /> +// Image +// </div> +// <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date)}> +// <FontAwesomeIcon icon={"calendar"} size="sm" /> +// Date +// </div> +// </div> +// </div > +// ); +// } + +// renderSorting = () => { +// const sort = this.props.columnField.desc; +// return ( +// <div className="collectionSchema-headerMenu-group"> +// <label>Sort by:</label> +// <div className="columnMenu-sort"> +// <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}> +// <FontAwesomeIcon icon="sort-amount-down" size="sm" /> +// Sort descending +// </div> +// <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}> +// <FontAwesomeIcon icon="sort-amount-up" size="sm" /> +// Sort ascending +// </div> +// <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}> +// <FontAwesomeIcon icon="times" size="sm" /> +// Clear sorting +// </div> +// </div> +// </div> +// ); +// } + +// renderColors = () => { +// const selected = this.props.columnField.color; + +// const pink = PastelSchemaPalette.get("pink2"); +// const purple = PastelSchemaPalette.get("purple2"); +// const blue = PastelSchemaPalette.get("bluegreen1"); +// const yellow = PastelSchemaPalette.get("yellow4"); +// const red = PastelSchemaPalette.get("red2"); +// const gray = "#f1efeb"; + +// return ( +// <div className="collectionSchema-headerMenu-group"> +// <label>Color:</label> +// <div className="columnMenu-colors"> +// <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div> +// <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div> +// <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div> +// <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div> +// <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div> +// <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div> +// </div> +// </div> +// ); +// } + +// renderContent = () => { +// return ( +// <div className="collectionSchema-header-menuOptions"> +// {this.props.onlyShowOptions ? <></> : +// <> +// {this.renderTypes()} +// {this.renderSorting()} +// {this.renderColors()} +// <div className="collectionSchema-headerMenu-group"> +// <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide Column</button> +// </div> +// </> +// } +// </div> +// ); +// } + +// render() { +// return ( +// <div className="collectionSchema-header-menu" ref={this.setNode}> +// <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}> +// <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div> +// </ Flyout > +// </div> +// ); +// } +// } + +// export interface KeysDropdownProps { +// keyValue: string; +// possibleKeys: string[]; +// existingKeys: string[]; +// canAddNew: boolean; +// addNew: boolean; +// onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void; +// setIsEditing: (isEditing: boolean) => void; +// width?: string; +// docs?: Doc[]; +// Document: Doc; +// dataDoc: Doc | undefined; +// fieldKey: string; +// ContainingCollectionDoc: Doc | undefined; +// ContainingCollectionView: Opt<CollectionView>; +// active?: (outsideReaction?: boolean) => boolean | undefined; +// openHeader: (column: any, screenx: number, screeny: number) => void; +// col: SchemaHeaderField; +// icon: IconProp; +// } +// @observer +// export class KeysDropdown extends React.Component<KeysDropdownProps> { +// @observable private _key: string = this.props.keyValue; +// @observable private _searchTerm: string = this.props.keyValue + ":"; +// @observable private _isOpen: boolean = false; +// @observable private _node: HTMLDivElement | null = null; +// @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); + +// @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; +// @action setKey = (key: string): void => { this._key = key; }; +// @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; }; + +// @action +// onSelect = (key: string): void => { +// this.props.onSelect(this._key, key, this.props.addNew); +// this.setKey(key); +// this._isOpen = false; +// this.props.setIsEditing(false); +// } + +// @action +// setNode = (node: HTMLDivElement): void => { +// if (node) { +// this._node = node; +// } +// } + +// componentDidMount() { +// document.addEventListener("pointerdown", this.detectClick); +// const filters = Cast(this.props.Document._docFilters, listSpec("string")); +// if (filters?.some(filter => filter.split(":")[0] === this._key)) { +// runInAction(() => this.closeResultsVisibility = "contents"); +// } +// } + +// @action +// detectClick = (e: PointerEvent): void => { +// if (this._node && this._node.contains(e.target as Node)) { +// } else { +// this._isOpen = false; +// this.props.setIsEditing(false); +// } +// } + +// private tempfilter: string = ""; +// @undoBatch +// onKeyDown = (e: React.KeyboardEvent): void => { +// if (e.key === "Enter") { +// e.stopPropagation(); +// if (this._searchTerm.includes(":")) { +// const colpos = this._searchTerm.indexOf(":"); +// const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); +// if (temp === "") { +// Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove"); +// this.updateFilter(); +// } +// else { +// Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove"); +// this.tempfilter = temp; +// Doc.setDocFilter(this.props.Document, this._key, temp, "check"); +// this.props.col.setColor("green"); +// this.closeResultsVisibility = "contents"; +// } +// } +// else { +// Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove"); +// this.updateFilter(); +// if (this.showKeys.length) { +// this.onSelect(this.showKeys[0]); +// } else if (this._searchTerm !== "" && this.props.canAddNew) { +// this.setSearchTerm(this._searchTerm || this._key); +// this.onSelect(this._searchTerm); +// } +// } +// } +// } + +// onChange = (val: string): void => { +// this.setSearchTerm(val); +// } + +// @action +// onFocus = (e: React.FocusEvent): void => { +// this._isOpen = true; +// this.props.setIsEditing(true); +// } + +// @computed get showKeys() { +// const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"]; +// const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); +// const showKeys = new Set<string>(); +// [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.noviceMode || +// whitelistKeys.includes(key) +// || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null); +// return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm)); +// } + +// @computed get renderOptions() { +// if (!this._isOpen) { +// this.defaultMenuHeight = 0; +// return (null); +// } +// const options = this.showKeys.map(key => { +// return <div key={key} className="key-option" style={{ +// border: "1px solid lightgray", +// width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", +// }} +// onPointerDown={e => { +// e.stopPropagation(); +// }} +// 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 (this._key !== this._searchTerm.slice(0, this._key.length)) { +// if (this._searchTerm !== "" && this.props.canAddNew) { +// options.push(<div key={""} className="key-option" style={{ +// border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", +// }} +// onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> +// Create "{this._searchTerm}" key</div>); +// } +// } + +// if (options.length === 0) { +// this.defaultMenuHeight = 0; +// } +// else { +// if (this.props.docs) { +// const panesize = this.props.docs.length * 30; +// options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8; +// } +// else { +// options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8; +// } +// } +// return options; +// } + +// @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); } + +// @computed get renderFilterOptions() { +// if (!this._isOpen || !this.props.dataDoc) { +// this.defaultMenuHeight = 0; +// return (null); +// } +// const keyOptions: string[] = []; +// const colpos = this._searchTerm.indexOf(":"); +// const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); +// this.docSafe.forEach(doc => { +// const key = StrCast(doc[this._key]); +// if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { +// keyOptions.push(key); +// } +// }); + +// const filters = StrListCast(this.props.Document._docFilters); +// if (filters.some(filter => filter.split(":")[0] === this._key) === false) { +// this.props.col.setColor("rgb(241, 239, 235)"); +// this.closeResultsVisibility = "none"; +// } +// for (let i = 0; i < (filters?.length ?? 0) - 1; i++) { +// if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) { +// keyOptions.push(filters[i + 1]); +// } +// } +// const options = keyOptions.map(key => { +// let bool = false; +// if (filters !== undefined) { +// const ind = filters.findIndex(filter => filter.split(":")[1] === key); +// const fields = ind === -1 ? undefined : filters[ind].split(":"); +// bool = fields ? fields[2] === "check" : false; +// } +// return <div key={key} className="key-option" style={{ +// paddingLeft: 5, textAlign: "left", +// width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", +// }} +// > +// <input type="checkbox" +// onPointerDown={e => e.stopPropagation()} +// onClick={e => e.stopPropagation()} +// onChange={action(e => { +// if (e.target.checked) { +// Doc.setDocFilter(this.props.Document, this._key, key, "check"); +// this.closeResultsVisibility = "contents"; +// this.props.col.setColor("green"); +// } else { +// Doc.setDocFilter(this.props.Document, this._key, key, "remove"); +// this.updateFilter(); +// } +// })} +// checked={bool} +// /> +// <span style={{ paddingLeft: 4 }}> +// {key} +// </span> + +// </div>; +// }); +// if (options.length === 0) { +// this.defaultMenuHeight = 0; +// } +// else { +// if (this.props.docs) { +// const panesize = this.props.docs.length * 30; +// options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8; +// } +// else { +// options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8; +// } + +// } +// return options; +// } + +// @observable defaultMenuHeight = 0; + +// updateFilter() { +// const filters = Cast(this.props.Document._docFilters, listSpec("string")); +// if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { +// this.props.col.setColor("rgb(241, 239, 235)"); +// this.closeResultsVisibility = "none"; +// } +// } + +// @computed get scriptField() { +// const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; +// const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); +// return script ? () => script : undefined; +// } +// filterBackground = () => "rgba(105, 105, 105, 0.432)"; +// @observable filterOpen: boolean | undefined = undefined; +// closeResultsVisibility: string = "none"; + +// removeFilters = (e: React.PointerEvent): void => { +// const keyOptions: string[] = []; +// this.docSafe.forEach(doc => { +// const key = StrCast(doc[this._key]); +// if (keyOptions.includes(key) === false) { +// keyOptions.push(key); +// } +// }); + +// Doc.setDocFilter(this.props.Document, this._key, "", "remove"); +// this.props.col.setColor("rgb(241, 239, 235)"); +// this.closeResultsVisibility = "none"; +// } +// render() { +// return ( +// <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}> +// <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}> +// <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} /> +// </div> + +// <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}> +// <input className="keys-search" style={{ width: "100%" }} +// ref={this._inputRef} type="text" +// value={this._searchTerm} placeholder="Column key" +// onKeyDown={this.onKeyDown} +// onChange={e => this.onChange(e.target.value)} +// onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} +// onFocus={this.onFocus} ></input> +// <div style={{ display: this.closeResultsVisibility }}> +// <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg" +// style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} /> +// </div> +// {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{ +// width: this.props.width, maxWidth: this.props.width, height: "auto", +// }}> +// {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions} +// </div>} +// </div > +// </div> +// ); +// } +// } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableColumn.tsx index 28d2e6ab1..28d2e6ab1 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx +++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableColumn.tsx diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableRow.tsx index f872637e5..f872637e5 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx +++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableRow.tsx diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss new file mode 100644 index 000000000..22ce8c8f2 --- /dev/null +++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss @@ -0,0 +1,599 @@ +@import '../../global/globalCssVariables.scss'; +// @import '../../../../../node_modules/react-table/react-table.css'; +.collectionSchemaView-container { + border-width: $COLLECTION_BORDER_WIDTH; + border-color: $medium-gray; + border-style: solid; + border-radius: $border-radius; + box-sizing: border-box; + position: relative; + top: 0; + width: 100%; + height: 100%; + margin-top: 0; + transition: top 0.5s; + display: flex; + justify-content: space-between; + flex-wrap: nowrap; + touch-action: none; + div { + touch-action: none; + } + .collectionSchemaView-tableContainer { + width: 100%; + height: 100%; + } + .collectionSchemaView-dividerDragger { + position: relative; + height: 100%; + width: $SCHEMA_DIVIDER_WIDTH; + z-index: 20; + right: 0; + top: 0; + background: gray; + cursor: col-resize; + } + // .documentView-node:first-child { + // background: $white; + // } +} + +.collectionSchemaView-searchContainer { + border-width: $COLLECTION_BORDER_WIDTH; + border-color: $medium-gray; + border-style: solid; + border-radius: $border-radius; + box-sizing: border-box; + position: relative; + top: 0; + width: 100%; + height: 100%; + margin-top: 0; + transition: top 0.5s; + display: flex; + justify-content: space-between; + flex-wrap: nowrap; + touch-action: none; + padding: 2px; + div { + touch-action: none; + } + .collectionSchemaView-tableContainer { + width: 100%; + height: 100%; + } + .collectionSchemaView-dividerDragger { + position: relative; + height: 100%; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: gray; + cursor: col-resize; + } + // .documentView-node:first-child { + // background: $white; + // } +} + +.ReactTable { + width: 100%; + background: white; + box-sizing: border-box; + border: none !important; + float: none !important; + .rt-table { + height: 100%; + display: -webkit-inline-box; + direction: ltr; + overflow: visible; + } + .rt-noData { + display: none; + } + .rt-thead { + width: 100%; + z-index: 100; + overflow-y: visible; + &.-header { + font-size: 12px; + height: 30px; + box-shadow: none; + z-index: 100; + overflow-y: visible; + } + .rt-resizable-header-content { + height: 100%; + overflow: visible; + } + .rt-th { + padding: 0; + border-left: solid 1px $light-gray; + } + } + .rt-th { + font-size: 13px; + text-align: center; + &:last-child { + overflow: visible; + } + } + .rt-tbody { + width: 100%; + direction: rtl; + overflow: visible; + .rt-td { + border-right: 1px solid rgba(0, 0, 0, 0.2); + } + } + .rt-tr-group { + direction: ltr; + flex: 0 1 auto; + min-height: 30px; + border: 0 !important; + } + .rt-tr-group:nth-of-type(even) { + direction: ltr; + flex: 0 1 auto; + min-height: 30px; + border: 0 !important; + background-color: red; + } + .rt-tr { + width: 100%; + min-height: 30px; + } + .rt-td { + padding: 0; + font-size: 13px; + text-align: center; + white-space: nowrap; + display: flex; + align-items: center; + .imageBox-cont { + position: relative; + max-height: 100%; + } + .imageBox-cont img { + object-fit: contain; + max-width: 100%; + height: 100%; + } + .videoBox-cont { + object-fit: contain; + width: auto; + height: 100%; + } + } + .rt-td.rt-expandable { + display: flex; + align-items: center; + height: inherit; + } + .rt-resizer { + width: 8px; + right: -4px; + } + .rt-resizable-header { + padding: 0; + height: 30px; + } + .rt-resizable-header:last-child { + overflow: visible; + .rt-resizer { + width: 5px !important; + } + } +} + +.documentView-node-topmost { + text-align: left; + transform-origin: center top; + display: inline-block; +} + +.collectionSchema-col { + height: 100%; +} + +.collectionSchema-header-menu { + height: auto; + z-index: 100; + position: absolute; + background: white; + padding: 5px; + position: fixed; + background: white; + border: black 1px solid; + .collectionSchema-header-toggler { + z-index: 100; + width: 100%; + height: 100%; + padding: 4px; + letter-spacing: 2px; + text-transform: uppercase; + svg { + margin-right: 4px; + } + } +} + +.collectionSchemaView-header { + height: 100%; + color: gray; + z-index: 100; + overflow-y: visible; + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +button.add-column { + width: 28px; +} + +.collectionSchemaView-menuOptions-wrapper { + background: rgb(241, 239, 235); + display: flex; + cursor: default; + height: 100%; + align-content: center; + align-items: center; +} + +.collectionSchema-header-menuOptions { + color: black; + width: 180px; + text-align: left; + .collectionSchema-headerMenu-group { + padding: 7px 0; + border-bottom: 1px solid lightgray; + cursor: pointer; + &:first-child { + padding-top: 0; + } + &:last-child { + border: none; + text-align: center; + padding: 12px 0 0 0; + } + } + label { + color: $medium-gray; + font-weight: normal; + letter-spacing: 2px; + text-transform: uppercase; + } + input { + color: black; + width: 100%; + } + .columnMenu-option { + cursor: pointer; + padding: 3px; + background-color: white; + transition: background-color 0.2s; + &:hover { + background-color: $light-gray; + } + &.active { + font-weight: bold; + border: 2px solid $light-gray; + } + svg { + color: gray; + margin-right: 5px; + width: 10px; + } + } + + .keys-dropdown { + position: relative; + //width: 100%; + background-color: white; + input { + border: 2px solid $light-gray; + padding: 3px; + height: 28px; + font-weight: bold; + letter-spacing: '2px'; + text-transform: 'uppercase'; + &:focus { + font-weight: normal; + } + } + } + .columnMenu-colors { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + .columnMenu-colorPicker { + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 10px; + &.active { + border: 2px solid white; + box-shadow: 0 0 0 2px lightgray; + } + } + } +} + +.schema-icon { + cursor: pointer; + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + background-color: $medium-blue; + color: white; + margin-right: 5px; + font-size: 10px; + border-radius: 3px; +} + +.keys-options-wrapper { + position: absolute; + text-align: left; + height: fit-content; + top: 100%; + z-index: 21; + background-color: #ffffff; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); + padding: 1px; + .key-option { + cursor: pointer; + color: #000000; + width: 100%; + height: 25px; + font-weight: 400; + display: flex; + justify-content: left; + align-items: center; + padding-left: 5px; + &:hover { + background-color: $light-gray; + } + } +} + +.collectionSchema-row { + height: 100%; + background-color: white; + &.row-focused .rt-td { + background-color: $light-blue; //$light-gray; + overflow: visible; + } + &.row-wrapped { + .rt-td { + white-space: normal; + } + } + .row-dragger { + display: flex; + justify-content: space-evenly; + width: 58px; + position: absolute; + /* max-width: 50px; */ + min-height: 30px; + align-items: center; + color: lightgray; + background-color: white; + transition: color 0.1s ease; + .row-option { + color: black; + cursor: pointer; + position: relative; + transition: color 0.1s ease; + display: flex; + flex-direction: column; + justify-content: center; + z-index: 2; + border-radius: 3px; + padding: 3px; + &:hover { + background-color: $light-gray; + } + } + } + .collectionSchema-row-wrapper { + &.row-above { + border-top: 1px solid $medium-blue; + } + &.row-below { + border-bottom: 1px solid $medium-blue; + } + &.row-inside { + border: 2px dashed $medium-blue; + } + .row-dragging { + background-color: blue; + } + } +} + +.collectionSchemaView-cellContainer { + width: 100%; + height: unset; +} + +.collectionSchemaView-cellContents { + width: 100%; +} + +.collectionSchemaView-cellWrapper { + display: flex; + height: 100%; + text-align: left; + padding-left: 19px; + position: relative; + align-items: center; + align-content: center; + &:focus { + outline: none; + } + &.editing { + padding: 0; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + transform: scale(1.1); + z-index: 40; + input { + outline: 0; + border: none; + background-color: $white; + width: 100%; + height: fit-content; + min-height: 26px; + } + } + &.focused { + overflow: hidden; + &.inactive { + border: none; + } + } + p { + width: 100%; + height: 100%; + } + &:hover .collectionSchemaView-cellContents-docExpander { + display: block; + } + .collectionSchemaView-cellContents-document { + display: inline-block; + } + .collectionSchemaView-cellContents-docButton { + float: right; + width: '15px'; + height: '15px'; + } + .collectionSchemaView-dropdownWrapper { + border: grey; + border-style: solid; + border-width: 1px; + height: 30px; + .collectionSchemaView-dropdownButton { + //display: inline-block; + float: left; + height: 100%; + } + .collectionSchemaView-dropdownText { + display: inline-block; + //float: right; + height: 100%; + display: 'flex'; + font-size: 13; + justify-content: 'center'; + align-items: 'center'; + } + } + .collectionSchemaView-dropdownContainer { + position: absolute; + border: 1px solid rgba(0, 0, 0, 0.04); + box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14); + .collectionSchemaView-dropdownOption:hover { + background-color: rgba(0, 0, 0, 0.14); + cursor: pointer; + } + } +} + +.collectionSchemaView-cellContents-docExpander { + height: 30px; + width: 30px; + display: none; + position: absolute; + top: 0; + right: 0; + background-color: lightgray; +} + +.doc-drag-over { + background-color: red; +} + +.collectionSchemaView-toolbar { + z-index: 100; +} + +.collectionSchemaView-toolbar { + height: 30px; + display: flex; + justify-content: flex-end; + padding: 0 10px; + border-bottom: 2px solid gray; + .collectionSchemaView-toolbar-item { + display: flex; + flex-direction: column; + justify-content: center; + } +} + +#preview-schema-checkbox-div { + margin-left: 20px; + font-size: 12px; +} + +.collectionSchemaView-table { + width: 100%; + height: 100%; + overflow: auto; + padding: 3px; +} + +.rt-td.rt-expandable { + overflow: visible; + position: relative; + height: 100%; + z-index: 1; +} + +.reactTable-sub { + background-color: rgb(252, 252, 252); + width: 100%; + .rt-thead { + display: none; + } + .row-dragger { + background-color: rgb(252, 252, 252); + } + .rt-table { + background-color: rgb(252, 252, 252); + } + .collectionSchemaView-table { + width: 100%; + border: solid 1px; + overflow: visible; + padding: 0px; + } +} + +.collectionSchemaView-expander { + height: 100%; + min-height: 30px; + position: absolute; + color: gray; + width: 20; + height: auto; + left: 55; + svg { + position: absolute; + top: 50%; + left: 10; + transform: translate(-50%, -50%); + } +} + +.collectionSchemaView-addRow { + color: gray; + letter-spacing: 2px; + text-transform: uppercase; + cursor: pointer; + font-size: 10.5px; + margin-left: 50px; + margin-top: 10px; +} diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx new file mode 100644 index 000000000..260db4b88 --- /dev/null +++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx @@ -0,0 +1,649 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, untracked } from 'mobx'; +import { observer } from 'mobx-react'; +import Measure from 'react-measure'; +// import { Resize } from 'react-table'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { listSpec } from '../../../../fields/Schema'; +import { PastelSchemaPalette, SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; +import { Cast, NumCast } from '../../../../fields/Types'; +import { TraceMobx } from '../../../../fields/util'; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; +import { DocUtils } from '../../../documents/Documents'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { Transform } from '../../../util/Transform'; +import { undoBatch } from '../../../util/UndoManager'; +import { ContextMenu } from '../../ContextMenu'; +import { ContextMenuProps } from '../../ContextMenuItem'; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; +import { DocumentView } from '../../nodes/DocumentView'; +import { DefaultStyleProvider } from '../../StyleProvider'; +import { CollectionSubView } from '../CollectionSubView'; +import './CollectionSchemaView.scss'; +// import { SchemaTable } from './SchemaTable'; +// 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, + Image, + List, + Date, +} +// this map should be used for keys that should have a const type of value +const columnTypes: Map<string, ColumnType> = new Map([ + ['title', ColumnType.String], + ['x', ColumnType.Number], + ['y', ColumnType.Number], + ['_width', ColumnType.Number], + ['_height', ColumnType.Number], + ['_nativeWidth', ColumnType.Number], + ['_nativeHeight', ColumnType.Number], + ['isPrototype', ColumnType.Boolean], + ['_curPage', ColumnType.Number], + ['_currentTimecode', ColumnType.Number], + ['zIndex', ColumnType.Number], +]); + +@observer +export class CollectionSchemaView extends CollectionSubView() { + private _previewCont?: HTMLDivElement; + + @observable _previewDoc: Doc | undefined = undefined; + @observable _focusedTable: Doc = this.props.Document; + @observable _col: any = ''; + @observable _menuWidth = 0; + @observable _headerOpen = false; + @observable _headerIsEditing = false; + @observable _menuHeight = 0; + @observable _pointerX = 0; + @observable _pointerY = 0; + @observable _openTypes: 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 - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); + } + @computed get borderWidth() { + return Number(COLLECTION_BORDER_WIDTH); + } + @computed get scale() { + return this.props.ScreenToLocalTransform().Scale; + } + @computed get columns() { + return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); + } + set columns(columns: SchemaHeaderField[]) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); + } + + @computed get menuCoordinates() { + let searchx = 0; + let searchy = 0; + if (this.props.Document._searchDoc) { + const el = document.getElementsByClassName('collectionSchemaView-searchContainer')[0]; + if (el !== undefined) { + const rect = el.getBoundingClientRect(); + searchx = rect.x; + searchy = rect.y; + } + } + const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx; + const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy; + return this.props.ScreenToLocalTransform().transformPoint(x, y); + } + + get documentKeys() { + const docs = this.childDocs; + const 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.heading] = true)); + return Array.from(Object.keys(keys)); + } + + @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing); + + @undoBatch + setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => { + this._openTypes = false; + if (columnTypes.get(columnField.heading)) return; + + const columns = this.columns; + const index = columns.indexOf(columnField); + if (index > -1) { + columnField.setType(NumCast(type)); + columns[index] = columnField; + this.columns = columns; + } + }); + + @undoBatch + setColumnColor = (columnField: SchemaHeaderField, color: string): void => { + const columns = this.columns; + const index = columns.indexOf(columnField); + if (index > -1) { + columnField.setColor(color); + columns[index] = columnField; + this.columns = columns; // need to set the columns to trigger rerender + } + }; + + @undoBatch + @action + setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { + const columns = this.columns; + columns.forEach(col => col.setDesc(undefined)); + + const index = columns.findIndex(c => c.heading === columnField.heading); + const column = columns[index]; + column.setDesc(descending); + columns[index] = column; + this.columns = columns; + }; + + renderTypes = (col: any) => { + if (columnTypes.get(col.heading)) return null; + + const type = col.type; + + const anyType = ( + <div className={'columnMenu-option' + (type === ColumnType.Any ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Any)}> + <FontAwesomeIcon icon={'align-justify'} size="sm" /> + Any + </div> + ); + + const numType = ( + <div className={'columnMenu-option' + (type === ColumnType.Number ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Number)}> + <FontAwesomeIcon icon={'hashtag'} size="sm" /> + Number + </div> + ); + + const textType = ( + <div className={'columnMenu-option' + (type === ColumnType.String ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.String)}> + <FontAwesomeIcon icon={'font'} size="sm" /> + Text + </div> + ); + + const boolType = ( + <div className={'columnMenu-option' + (type === ColumnType.Boolean ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Boolean)}> + <FontAwesomeIcon icon={'check-square'} size="sm" /> + Checkbox + </div> + ); + + const listType = ( + <div className={'columnMenu-option' + (type === ColumnType.List ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.List)}> + <FontAwesomeIcon icon={'list-ul'} size="sm" /> + List + </div> + ); + + const docType = ( + <div className={'columnMenu-option' + (type === ColumnType.Doc ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Doc)}> + <FontAwesomeIcon icon={'file'} size="sm" /> + Document + </div> + ); + + const imageType = ( + <div className={'columnMenu-option' + (type === ColumnType.Image ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Image)}> + <FontAwesomeIcon icon={'image'} size="sm" /> + Image + </div> + ); + + const dateType = ( + <div className={'columnMenu-option' + (type === ColumnType.Date ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Date)}> + <FontAwesomeIcon icon={'calendar'} size="sm" /> + Date + </div> + ); + + const allColumnTypes = ( + <div className="columnMenu-types"> + {anyType} + {numType} + {textType} + {boolType} + {listType} + {docType} + {imageType} + {dateType} + </div> + ); + + const justColType = + type === ColumnType.Any + ? anyType + : type === ColumnType.Number + ? numType + : type === ColumnType.String + ? textType + : type === ColumnType.Boolean + ? boolType + : type === ColumnType.List + ? listType + : type === ColumnType.Doc + ? docType + : type === ColumnType.Date + ? dateType + : imageType; + + return ( + <div className="collectionSchema-headerMenu-group" onClick={action(() => (this._openTypes = !this._openTypes))}> + <div> + <label style={{ cursor: 'pointer' }}>Column type:</label> + <FontAwesomeIcon icon={'caret-down'} size="lg" style={{ float: 'right', transform: `rotate(${this._openTypes ? '180deg' : 0})`, transition: '0.2s all ease' }} /> + </div> + {this._openTypes ? allColumnTypes : justColType} + </div> + ); + }; + + renderSorting = (col: any) => { + const sort = col.desc; + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Sort by:</label> + <div className="columnMenu-sort"> + <div className={'columnMenu-option' + (sort === true ? ' active' : '')} onClick={() => this.setColumnSort(col, true)}> + <FontAwesomeIcon icon="sort-amount-down" size="sm" /> + Sort descending + </div> + <div className={'columnMenu-option' + (sort === false ? ' active' : '')} onClick={() => this.setColumnSort(col, false)}> + <FontAwesomeIcon icon="sort-amount-up" size="sm" /> + Sort ascending + </div> + <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}> + <FontAwesomeIcon icon="times" size="sm" /> + Clear sorting + </div> + </div> + </div> + ); + }; + + renderColors = (col: any) => { + const selected = col.color; + + const pink = PastelSchemaPalette.get('pink2'); + const purple = PastelSchemaPalette.get('purple2'); + const blue = PastelSchemaPalette.get('bluegreen1'); + const yellow = PastelSchemaPalette.get('yellow4'); + const red = PastelSchemaPalette.get('red2'); + const gray = '#f1efeb'; + + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Color:</label> + <div className="columnMenu-colors"> + <div className={'columnMenu-colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div> + <div className={'columnMenu-colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div> + <div className={'columnMenu-colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div> + <div className={'columnMenu-colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div> + <div className={'columnMenu-colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div> + <div className={'columnMenu-colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div> + </div> + </div> + ); + }; + + @undoBatch + @action + changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { + const columns = this.columns; + if (columns === undefined) { + this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, 'f1efeb')]); + } else { + if (addNew) { + columns.push(new SchemaHeaderField(newKey, 'f1efeb')); + this.columns = columns; + } else { + const index = columns.map(c => c.heading).indexOf(oldKey); + if (index > -1) { + const column = columns[index]; + column.setHeading(newKey); + columns[index] = column; + this.columns = columns; + if (filter) { + Doc.setDocFilter(this.props.Document, newKey, filter, 'match'); + } else { + this.props.Document._docFilters = undefined; + } + } + } + } + }; + + @action + openHeader = (col: any, screenx: number, screeny: number) => { + this._col = col; + this._headerOpen = true; + this._pointerX = screenx; + this._pointerY = screeny; + }; + + @action + closeHeader = () => { + this._headerOpen = false; + }; + + @undoBatch + @action + deleteColumn = (key: string) => { + const columns = this.columns; + if (columns === undefined) { + this.columns = new List<SchemaHeaderField>([]); + } else { + const index = columns.map(c => c.heading).indexOf(key); + if (index > -1) { + columns.splice(index, 1); + this.columns = columns; + } + } + this.closeHeader(); + }; + + getPreviewTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(-this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth); + }; + + @action + onHeaderClick = (e: React.PointerEvent) => { + e.stopPropagation(); + }; + + @action + onWheel(e: React.WheelEvent) { + const scale = this.props.ScreenToLocalTransform().Scale; + this.props.isContentActive(true) && e.stopPropagation(); + } + + @computed get renderMenuContent() { + TraceMobx(); + return ( + <div className="collectionSchema-header-menuOptions"> + {this.renderTypes(this._col)} + {this.renderColors(this._col)} + <div className="collectionSchema-headerMenu-group"> + <button + onClick={() => { + this.deleteColumn(this._col.heading); + }}> + Hide Column + </button> + </div> + </div> + ); + } + + private createTarget = (ele: HTMLDivElement) => { + this._previewCont = ele; + super.CreateDropTarget(ele); + }; + + isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable; + + @action setFocused = (doc: Doc) => (this._focusedTable = doc); + + @action setPreviewDoc = (doc: Opt<Doc>) => { + SelectionManager.SelectSchemaViewDoc(doc); + this._previewDoc = doc; + }; + + //toggles preview side-panel of schema + @action + toggleExpander = () => { + this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; + }; + + onDividerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander); + }; + @action + onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { + const nativeWidth = this._previewCont!.getBoundingClientRect(); + const minWidth = 40; + const maxWidth = 1000; + const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; + const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; + this.props.Document.schemaPreviewWidth = width; + return false; + }; + + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (this.props.isSelected(true)) e.stopPropagation(); + else this.props.select(false); + } + }; + + @computed + get previewDocument(): Doc | undefined { + return this._previewDoc; + } + + @computed + get dividerDragger() { + return this.previewWidth() === 0 ? null : ( + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown}> + <div className="collectionSchemaView-dividerDragger" /> + </div> + ); + } + + @computed + get previewPanel() { + return ( + <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}> + {!this.previewDocument ? null : ( + <DocumentView + Document={this.previewDocument} + DataDoc={undefined} + fitContentsToBox={returnTrue} + dontCenter={'y'} + focus={DocUtils.DefaultFocus} + renderDepth={this.props.renderDepth} + rootSelected={this.rootSelected} + PanelWidth={this.previewWidth} + PanelHeight={this.previewHeight} + isContentActive={returnTrue} + isDocumentActive={returnFalse} + ScreenToLocalTransform={this.getPreviewTransform} + docFilters={this.childDocFilters} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + styleProvider={DefaultStyleProvider} + docViewPath={returnEmptyDoclist} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + /> + )} + </div> + ); + } + + @computed + get schemaTable() { + return ( + <SchemaTable + Document={this.props.Document} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + childDocs={this.childDocs} + CollectionView={this.props.CollectionView} + ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + fieldKey={this.props.fieldKey} + renderDepth={this.props.renderDepth} + moveDocument={this.props.moveDocument} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + active={this.props.isContentActive} + onDrop={this.onExternalDrop} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + isSelected={this.props.isSelected} + isFocused={this.isFocused} + setFocused={this.setFocused} + setPreviewDoc={this.setPreviewDoc} + deleteDocument={this.props.removeDocument} + addDocument={this.props.addDocument} + dataDoc={this.props.DataDoc} + columns={this.columns} + documentKeys={this.documentKeys} + headerIsEditing={this._headerIsEditing} + openHeader={this.openHeader} + onClick={this.onTableClick} + onPointerDown={emptyFunction} + onResizedChange={this.onResizedChange} + setColumns={this.setColumns} + reorderColumns={this.reorderColumns} + changeColumns={this.changeColumns} + setHeaderIsEditing={this.setHeaderIsEditing} + changeColumnSort={this.setColumnSort} + /> + ); + } + + @computed + public get schemaToolbar() { + return ( + <div className="collectionSchemaView-toolbar"> + <div className="collectionSchemaView-toolbar-item"> + <div id="preview-schema-checkbox-div"> + <input type="checkbox" key={'Show Preview'} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> + Show Preview + </div> + </div> + </div> + ); + } + + onSpecificMenu = (e: React.MouseEvent) => { + if ((e.target as any)?.className?.includes?.('collectionSchemaView-cell') || e.target instanceof HTMLSpanElement) { + const cm = ContextMenu.Instance; + const options = cm.findByDescription('Options...'); + const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + optionItems.push({ description: 'remove', event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: 'trash' }); + !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); + cm.displayMenu(e.clientX, e.clientY); + (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. + e.stopPropagation(); + } + }; + + @action + onTableClick = (e: React.MouseEvent): void => { + if (!(e.target as any)?.className?.includes?.('collectionSchemaView-cell') && !(e.target instanceof HTMLSpanElement)) { + this.setPreviewDoc(undefined); + } else { + e.stopPropagation(); + } + this.setFocused(this.props.Document); + this.closeHeader(); + }; + + onResizedChange = (newResized: Resize[], event: any) => { + const columns = this.columns; + newResized.forEach(resized => { + const index = columns.findIndex(c => c.heading === resized.id); + const column = columns[index]; + column.setWidth(resized.value); + columns[index] = column; + }); + this.columns = columns; + }; + + @action + setColumns = (columns: SchemaHeaderField[]) => (this.columns = columns); + + @undoBatch + reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => { + const columns = [...columnsValues]; + const oldIndex = columns.indexOf(toMove); + const relIndex = columns.indexOf(relativeTo); + const 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.columns = columns; + }; + + onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation(); + + render() { + TraceMobx(); + if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0); + const menuContent = this.renderMenuContent; + const menu = ( + <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ transform: `translate(${this.menuCoordinates[0]}px, ${this.menuCoordinates[1]}px)` }}> + <Measure + offset + onResize={action((r: any) => { + const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); + this._menuWidth = dim[0]; + this._menuHeight = dim[1]; + })}> + {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} + </Measure> + </div> + ); + return ( + <div + className={'collectionSchemaView' + (this.props.Document._searchDoc ? '-searchContainer' : '-container')} + style={{ + overflow: this.props.scrollOverflow === true ? 'scroll' : undefined, + backgroundColor: 'white', + pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined, + width: this.props.PanelWidth() || '100%', + height: this.props.PanelHeight() || '100%', + position: 'relative', + }}> + <div + className="collectionSchemaView-tableContainer" + style={{ width: `calc(100% - ${this.previewWidth()}px)` }} + onContextMenu={this.onSpecificMenu} + onPointerDown={this.onPointerDown} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} + onDrop={e => this.onExternalDrop(e, {})} + ref={this.createTarget}> + {this.schemaTable} + </div> + {this.dividerDragger} + {!this.previewWidth() ? null : this.previewPanel} + {this._headerOpen && this.props.isContentActive() ? menu : null} + </div> + ); + TraceMobx(); + return <div>HELLO</div>; + } +} diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/old_collectionSchema/OldSchemaTable.tsx index fafea5ce3..dfeee3173 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/old_collectionSchema/OldSchemaTable.tsx @@ -39,7 +39,7 @@ import { CollectionSchemaStringCell, } from './CollectionSchemaCells'; import { CollectionSchemaAddColumnHeader, KeysDropdown } from './CollectionSchemaHeaders'; -import { MovableColumn } from './CollectionSchemaMovableColumn'; +import { MovableColumn } from './OldCollectionSchemaMovableColumn'; import { MovableRow } from './CollectionSchemaMovableRow'; import './CollectionSchemaView.scss'; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 8437736ae..7bbd7c055 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -18,6 +18,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import './AudioBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { SelectionManager } from '../../util/SelectionManager'; /** * AudioBox |