From 7f42c98318899a51cb64080fc4f4e315e43bb150 Mon Sep 17 00:00:00 2001 From: 0x85FB9C51 <77808164+0x85FB9C51@users.noreply.github.com> Date: Tue, 6 Jul 2021 23:47:38 -0400 Subject: reoganization of schema view files, partial --- .../schemaView/CollectionSchemaView.tsx | 575 +++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 src/client/views/collections/schemaView/CollectionSchemaView.tsx (limited to 'src/client/views/collections/schemaView/CollectionSchemaView.tsx') diff --git a/src/client/views/collections/schemaView/CollectionSchemaView.tsx b/src/client/views/collections/schemaView/CollectionSchemaView.tsx new file mode 100644 index 000000000..b33c437a9 --- /dev/null +++ b/src/client/views/collections/schemaView/CollectionSchemaView.tsx @@ -0,0 +1,575 @@ +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 "react-table/react-table.css"; +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, emptyPath, returnFalse, setupMoveUpEvents, returnEmptyDoclist, returnTrue } from "../../../Utils"; +import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import '../DocumentDecorations.scss'; +import { DocumentView } from "../nodes/DocumentView"; +import { DefaultStyleProvider } from "../StyleProvider"; +import "./CollectionSchemaView.scss"; +import { CollectionSubView } from "./CollectionSubView"; +import { SchemaTable } from "./SchemaTable"; +import { DocUtils } from "../../documents/Documents"; +// 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 = 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(doc => doc) { + 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(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 =
this.setColumnType(col, ColumnType.Any)}> + + Any +
; + + const numType =
this.setColumnType(col, ColumnType.Number)}> + + Number +
; + + const textType =
this.setColumnType(col, ColumnType.String)}> + + Text +
; + + const boolType =
this.setColumnType(col, ColumnType.Boolean)}> + + Checkbox +
; + + const listType =
this.setColumnType(col, ColumnType.List)}> + + List +
; + + const docType =
this.setColumnType(col, ColumnType.Doc)}> + + Document +
; + + const imageType =
this.setColumnType(col, ColumnType.Image)}> + + Image +
; + + const dateType =
this.setColumnType(col, ColumnType.Date)}> + + Date +
; + + + const allColumnTypes =
+ {anyType} + {numType} + {textType} + {boolType} + {listType} + {docType} + {imageType} + {dateType} +
; + + 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 ( +
this._openTypes = !this._openTypes)}> +
+ + +
+ {this._openTypes ? allColumnTypes : justColType} +
+ ); + } + + renderSorting = (col: any) => { + const sort = col.desc; + return ( +
+ +
+
this.setColumnSort(col, true)}> + + Sort descending +
+
this.setColumnSort(col, false)}> + + Sort ascending +
+
this.setColumnSort(col, undefined)}> + + Clear sorting +
+
+
+ ); + } + + 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 ( +
+ +
+
this.setColumnColor(col, pink!)}>
+
this.setColumnColor(col, purple!)}>
+
this.setColumnColor(col, blue!)}>
+
this.setColumnColor(col, yellow!)}>
+
this.setColumnColor(col, red!)}>
+
this.setColumnColor(col, gray)}>
+
+
+ ); + } + + @undoBatch + @action + changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { + const columns = this.columns; + if (columns === undefined) { + this.columns = new List([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([]); + } 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
+ {this.renderTypes(this._col)} + {this.renderColors(this._col)} +
+ +
+
; + } + + 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) => { + SelectionManager.SelectSchemaView(this, 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) : +
+
+
; + } + + @computed + get previewPanel() { + return
+ {!this.previewDocument ? (null) : + } +
; + } + + @computed + get schemaTable() { + return ; + } + + @computed + public get schemaToolbar() { + return
+
+
+ + Show Preview +
+
+
; + } + + 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 =
this.onZoomMenu(e)} + onPointerDown={e => this.onHeaderClick(e)} + style={{ transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` }}> + { + const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); + this._menuWidth = dim[0]; this._menuHeight = dim[1]; + })}> + {({ measureRef }) =>
{menuContent}
} +
+
; + return
+
this.props.isContentActive(true) && e.stopPropagation()} + onDrop={e => this.onExternalDrop(e, {})} + ref={this.createTarget}> + {this.schemaTable} +
+ {this.dividerDragger} + {!this.previewWidth() ? (null) : this.previewPanel} + {this._headerOpen && this.props.isContentActive() ? menu : null} +
; + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2