From e48f447f66f7f50f39be385e0eb09df552f5f503 Mon Sep 17 00:00:00 2001 From: geireann Date: Mon, 12 Jul 2021 13:54:23 -0400 Subject: Changed "schemaView" :arrow_right: "collectionSchema" So style is consistent with other folders --- src/client/util/SelectionManager.ts | 2 +- .../views/collections/CollectionSchemaView.tsx | 575 ++++++++++++++++++++ src/client/views/collections/CollectionView.tsx | 2 +- .../collectionSchema/CollectionSchemaCells.tsx | 585 ++++++++++++++++++++ .../collectionSchema/CollectionSchemaHeaders.tsx | 518 ++++++++++++++++++ .../CollectionSchemaMovableColumn.tsx | 128 +++++ .../CollectionSchemaMovableRow.tsx | 147 +++++ .../collectionSchema/CollectionSchemaView.scss | 552 +++++++++++++++++++ .../collectionSchema/CollectionSchemaView.tsx | 575 ++++++++++++++++++++ .../collections/collectionSchema/SchemaTable.tsx | 601 +++++++++++++++++++++ .../schemaView/CollectionSchemaCells.tsx | 585 -------------------- .../schemaView/CollectionSchemaHeaders.tsx | 518 ------------------ .../schemaView/CollectionSchemaMovableColumn.tsx | 128 ----- .../schemaView/CollectionSchemaMovableRow.tsx | 147 ----- .../schemaView/CollectionSchemaView.scss | 552 ------------------- .../schemaView/CollectionSchemaView.tsx | 575 -------------------- .../views/collections/schemaView/SchemaTable.tsx | 601 --------------------- src/client/views/nodes/DocumentContentsView.tsx | 2 +- src/client/views/search/SearchBox.tsx | 8 +- src/fields/SchemaHeaderField.ts | 2 +- 20 files changed, 3689 insertions(+), 3114 deletions(-) create mode 100644 src/client/views/collections/CollectionSchemaView.tsx create mode 100644 src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx create mode 100644 src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx create mode 100644 src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx create mode 100644 src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx create mode 100644 src/client/views/collections/collectionSchema/CollectionSchemaView.scss create mode 100644 src/client/views/collections/collectionSchema/CollectionSchemaView.tsx create mode 100644 src/client/views/collections/collectionSchema/SchemaTable.tsx delete mode 100644 src/client/views/collections/schemaView/CollectionSchemaCells.tsx delete mode 100644 src/client/views/collections/schemaView/CollectionSchemaHeaders.tsx delete mode 100644 src/client/views/collections/schemaView/CollectionSchemaMovableColumn.tsx delete mode 100644 src/client/views/collections/schemaView/CollectionSchemaMovableRow.tsx delete mode 100644 src/client/views/collections/schemaView/CollectionSchemaView.scss delete mode 100644 src/client/views/collections/schemaView/CollectionSchemaView.tsx delete mode 100644 src/client/views/collections/schemaView/SchemaTable.tsx (limited to 'src') diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index a624d5b7c..00f0894c7 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,7 +1,7 @@ import { action, observable, ObservableMap } from "mobx"; import { computedFn } from "mobx-utils"; import { Doc, Opt } from "../../fields/Doc"; -import { CollectionSchemaView } from "../views/collections/schemaView/CollectionSchemaView"; +import { CollectionSchemaView } from "../views/collections/collectionSchema/CollectionSchemaView"; import { CollectionViewType } from "../views/collections/CollectionView"; import { DocumentView } from "../views/nodes/DocumentView"; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx new file mode 100644 index 000000000..8f2847139 --- /dev/null +++ b/src/client/views/collections/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 diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index e5b1721f9..e225c4a11 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -29,7 +29,7 @@ import CollectionMapView from './CollectionMapView'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionPileView } from './CollectionPileView'; -import { CollectionSchemaView } from "./schemaView/CollectionSchemaView"; +import { CollectionSchemaView } from "./collectionSchema/CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx new file mode 100644 index 000000000..f75179cea --- /dev/null +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -0,0 +1,585 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { CellInfo } from "react-table"; +import "react-table/react-table.css"; +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, NumCast, StrCast } from "../../../../fields/Types"; +import { ImageField } from "../../../../fields/URLField"; +import { Utils, emptyFunction } 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 { MAX_ROW_HEIGHT } from '../../globalCssVariables.scss'; +import { DocumentIconContainer } from "../../nodes/DocumentIcon"; +import { OverlayView } from "../../OverlayView"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "../CollectionView"; +const path = require('path'); + +// intialize cell properties +export interface CellProps { + row: number; + col: number; + rowProps: CellInfo; + CollectionView: Opt; + ContainingCollection: Opt; + Document: Doc; + fieldKey: string; + renderDepth: number; + addDocTab: (document: Doc, where: string) => boolean; + pinToPres: (document: Doc) => void; + moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, + addDocument: (document: Doc | Doc[]) => boolean) => boolean; + isFocused: boolean; + changeFocusedCellByIndex: (row: number, col: number) => void; + setIsEditing: (isEditing: boolean) => void; + isEditable: boolean; + setPreviewDoc: (doc: Doc) => void; + setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean; + getField: (row: number, col?: number) => void; + showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void; +} + +@observer +export class CollectionSchemaCell extends React.Component { + public static resolvedFieldKey(column: string, rowDoc: Doc) { + const fieldKey = column; + 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)); + if (matchedKeys.length) return matchedKeys[0]; + } + return fieldKey; + } + @observable protected _isEditing: boolean = false; + protected _focusRef = React.createRef(); + protected _rowDoc = this.props.rowProps.original; + protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); + protected _dropDisposer?: DragManager.DragDropDisposer; + @observable contents: string = ""; + + componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } + componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } + + @action + onKeyDown = (e: KeyboardEvent): void => { + if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) { + document.removeEventListener("keydown", this.onKeyDown); + this._isEditing = true; + this.props.setIsEditing(true); + } + } + + @action + isEditingCallback = (isEditing: boolean): void => { + 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); + } + + @action + onPointerDown = async (e: React.PointerEvent): Promise => { + this.onItemDown(e); + this.props.changeFocusedCellByIndex(this.props.row, this.props.col); + this.props.setPreviewDoc(this.props.rowProps.original); + + let url: string; + if (url = StrCast(this.props.rowProps.row.href)) { + try { + new URL(url); + const temp = window.open(url)!; + temp.blur(); + window.focus(); + } 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) => { + const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) }); + if (!res.success) return false; + doc[this.renderFieldKey] = res.result; + return true; + } + + private drop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + if (de.complete.docDragData.draggedDocuments.length === 1) { + this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; + } + else { + 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) => { + this._dropDisposer?.(); + 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"; + + results.push({contents?.slice(0, positions[0])}); + positions.forEach((num, cur) => { + results.push({contents?.slice(num, num + length)}); + let end = 0; + cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; + results.push({contents?.slice(num + length, end)}); + } + ); + return results; + } + return {contents ? contents?.valueOf() : "undefined"}; + } + + @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); } + onItemDown = async (e: React.PointerEvent) => { + if (this.props.Document._searchDoc) { + const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); + const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext, + undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + } + } + renderCellWithType(type: string | undefined) { + const dragRef: React.RefObject = React.createRef(); + + const fieldKey = this.renderFieldKey; + const field = this._rowDoc[fieldKey]; + + const onPointerEnter = (e: React.PointerEvent): void => { + if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { + dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; + } + }; + const onPointerLeave = (e: React.PointerEvent): void => { + dragRef.current!.className = "collectionSchemaView-cellContainer"; + }; + + let contents = Field.toString(field as Field); + contents = contents === "" ? "--" : contents; + + 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() !== "") { + 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; + if (start !== -1) { + positions.push(start); + } + while (start < contents?.length && start !== -1) { + term = term.slice(start + search.length + 1); + tally += start + search.length + 1; + start = term.indexOf(search); + positions.push(tally + start); + } + if (positions.length > 1) { + positions.pop(); + } + } + const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; + return ( +
this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> +
+
+ {!this.props.Document._searchDoc ? + { + 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) : ""; + }} + SetValue={action((value: string) => { + // sets what is displayed after the user makes an input + let retVal = false; + 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); + } else { + // check if the input is a number + let inputIsNum = true; + for (let s of value) { + if (isNaN(parseInt(s)) && !(s == ".") && !(s == ",")) { + inputIsNum = false; + } + } + // check if the input is a boolean + let 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 + // 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); + } + let inputAsString = '"'; + // escape any quotes in the string + for (const i of valueSansQuotes) { + if (i == '"') { + inputAsString += '\\"'; + } else { + inputAsString += i; + } + } + // 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 changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length + 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("=")) { + //TODO: make accept numbers + 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 = ""; + for (let s of inputscript) { + 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 changeMade = value.length !== value.length || 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 changeMade = value.length !== value.length || value.length - 2 !== value.length + script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); + } + } + if (retVal) { + this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' + this.props.setIsEditing(false); + } + return retVal; + })} + OnFillDown={async (value: string) => { + 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) + } +
+
+
+ ); + } + + render() { return this.renderCellWithType(undefined); } +} + +@observer +export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } + +@observer +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } + +@observer +export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } + +@observer +export class CollectionSchemaDateCell extends CollectionSchemaCell { + @computed get _date(): Opt { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; } + + @action + handleChange = (date: any) => { + // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); + // if (script.compiled) { + // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); + // } else { + // ^ 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 ? {this._date ? Field.toString(this._date as Field) : "--"} : + 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)); } + + @action + onSetValue = (value: string) => { + this._doc && (Doc.GetProto(this._doc).title = value); + + const script = CompileScript(value, { + addReturn: true, + typecheck: true, + transformer: DocumentIconContainer.getTransformer() + }); + + const results = script.compiled && script.run(); + if (results && results.success) { + this._rowDoc[this.renderFieldKey] = results.result; + return true; + } + return false; + } + + componentWillUnmount() { this.onBlur(); } + + onBlur = () => { this._overlayDisposer?.(); }; + onFocus = () => { + this.onBlur(); + this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); + } + + @action + isEditingCallback = (isEditing: boolean): void => { + 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() { + return !this._doc ? this.renderCellWithType("document") : +
+
+ StrCast(this._doc?.title)} + SetValue={action((value: string) => { + this.onSetValue(value); + return true; + })} + /> +
+
this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> + +
+
; + } +} + +@observer +export class CollectionSchemaImageCell extends CollectionSchemaCell { + + choosePath(url: URL) { + if (url.protocol === "data") return url.href; + if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here + + const ext = path.extname(url.href); + return url.href.replace(ext, "_o" + path.extname(url.href)); + } + + 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 paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; + const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + + const aspect = Doc.NativeAspect(this._rowDoc); + let width = Math.min(75, this.props.rowProps.width); + const height = Math.min(75, width / aspect); + width = height * aspect; + + const reference = React.createRef(); + return
+
+ +
+
; + } +} + + +@observer +export class CollectionSchemaListCell extends CollectionSchemaCell { + _overlayDisposer?: () => void; + + @computed get _field() { return this._rowDoc[this.renderFieldKey]; } + @computed get _optionsList() { return this._field as List; } + @observable private _opened = false; + @observable private _text = "select an item"; + @observable private _selectedNum = 0; + + @action + onSetValue = (value: string) => { + // change if its a document + this._optionsList[this._selectedNum] = this._text = value; + + (this._field as List).splice(this._selectedNum, 1, value); + } + + @action + onSelected = (element: string, index: number) => { + this._text = element; + this._selectedNum = index; + } + + onFocus = () => { + this._overlayDisposer?.(); + this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); + } + + render() { + const link = false; + const reference = React.createRef(); + + if (this._optionsList?.length) { + const options = !this._opened ? (null) : +
+ {this._optionsList.map((element, index) => { + const val = Field.toString(element); + return
this.onSelected(StrCast(element), index)} > + {val} +
; + })} +
; + + const plainText =
{this._text}
; + const textarea =
+ this._text} + SetValue={action((value: string) => { + // add special for params + this.onSetValue(value); + return true; + })} + /> +
; + + //☰ + return ( +
+
+
+ +
{link ? plainText : textarea}
+
+ {options} +
+
+ ); + } + return this.renderCellWithType("list"); + } +} + + +@observer +export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { + @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } + + render() { + const reference = React.createRef(); + return ( +
+ this._rowDoc[this.renderFieldKey] = e.target.checked} /> +
+ ); + } +} + + +@observer +export class CollectionSchemaButtons extends CollectionSchemaCell { + render() { + return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <> : +
+ + +
; + } +} \ No newline at end of file diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx new file mode 100644 index 000000000..b2115b22e --- /dev/null +++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx @@ -0,0 +1,518 @@ +import React = require("react"); +import { IconProp, library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast, Opt } 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 { SearchBox } from "../../search/SearchBox"; +import { ColumnType } from "./CollectionSchemaView"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "../CollectionView"; + +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 { + render() { + return ( + + ); + } +} + + +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 { + @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 ( +
+ +
+
this.changeColumnType(ColumnType.Any)}> + + Any +
+
this.changeColumnType(ColumnType.Number)}> + + Number +
+
this.changeColumnType(ColumnType.String)}> + + Text +
+
this.changeColumnType(ColumnType.Boolean)}> + + Checkbox +
+
this.changeColumnType(ColumnType.List)}> + + List +
+
this.changeColumnType(ColumnType.Doc)}> + + Document +
+
this.changeColumnType(ColumnType.Image)}> + + Image +
+
this.changeColumnType(ColumnType.Date)}> + + Date +
+
+
+ ); + } + + renderSorting = () => { + const sort = this.props.columnField.desc; + return ( +
+ +
+
this.changeColumnSort(true)}> + + Sort descending +
+
this.changeColumnSort(false)}> + + Sort ascending +
+
this.changeColumnSort(undefined)}> + + Clear sorting +
+
+
+ ); + } + + 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 ( +
+ +
+
this.changeColumnColor(pink!)}>
+
this.changeColumnColor(purple!)}>
+
this.changeColumnColor(blue!)}>
+
this.changeColumnColor(yellow!)}>
+
this.changeColumnColor(red!)}>
+
this.changeColumnColor(gray)}>
+
+
+ ); + } + + renderContent = () => { + return ( +
+ {this.props.onlyShowOptions ? <> : + <> + {this.renderTypes()} + {this.renderSorting()} + {this.renderColors()} +
+ +
+ + } +
+ ); + } + + render() { + return ( +
+ +
this.toggleIsOpen()}>{this.props.menuButtonContent}
+ +
+ ); + } +} + + +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; + active?: (outsideReaction?: boolean) => boolean; + openHeader: (column: any, screenx: number, screeny: number) => void; + col: SchemaHeaderField; + icon: IconProp; +} +@observer +export class KeysDropdown extends React.Component { + @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 = 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") { + 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(); + [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.UserDoc().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)); + } + @action + renderOptions = (): JSX.Element[] | JSX.Element => { + if (!this._isOpen) { + this.defaultMenuHeight = 0; + return <>; + } + const options = this.showKeys.map(key => { + return
{ + e.stopPropagation(); + }} + onClick={() => { + this.onSelect(key); + this.setSearchTerm(""); + }}>{key}
; + }); + + // 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(
{ this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> + Create "{this._searchTerm}" key
); + } + } + + 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; + } + + docSafe: Doc[] = []; + + @action + renderFilterOptions = (): JSX.Element[] | JSX.Element => { + if (!this._isOpen || !this.props.dataDoc) { + this.defaultMenuHeight = 0; + return <>; + } + const keyOptions: string[] = []; + const colpos = this._searchTerm.indexOf(":"); + const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); + if (this.docSafe.length === 0) { + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); + } + const docs = this.docSafe; + docs.forEach((doc) => { + const key = StrCast(doc[this._key]); + if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { + keyOptions.push(key); + } + }); + + 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"; + } + 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(":")[0] === key); + const fields = ind === -1 ? undefined : filters[ind].split(":"); + bool = fields ? fields[1] === "check" : false; + } + return
+ e.stopPropagation()} + onClick={e => e.stopPropagation()} + onChange={(e) => { + e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove"); + e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log(""); + e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter(); + e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove"); + }} + checked={bool} + /> + + {key} + + +
; + }); + 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[] = []; + if (this.docSafe.length === 0 && this.props.dataDoc) { + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); + } + const docs = this.docSafe; + docs.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 ( +
+ { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> + + {/* { + runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) + }} /> */} + +
+ this.onChange(e.target.value)} + onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} + onFocus={this.onFocus} > +
+ +
+ {!this._isOpen ? (null) :
+ {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()} +
} +
+
+ ); + } +} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx new file mode 100644 index 000000000..456c38c68 --- /dev/null +++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx @@ -0,0 +1,128 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; +import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; +import { Doc } from "../../../../fields/Doc"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { Cast, FieldValue, StrCast } from "../../../../fields/Types"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { DragManager, dropActionType, SetupDrag } from "../../../util/DragManager"; +import { SnappingManager } from "../../../util/SnappingManager"; +import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; +import { ContextMenu } from "../../ContextMenu"; +import "./CollectionSchemaView.scss"; + +export interface MovableColumnProps { + columnRenderer: TableCellRenderer; + columnValue: SchemaHeaderField; + allColumns: SchemaHeaderField[]; + reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void; + ScreenToLocalTransform: () => Transform; +} +export class MovableColumn extends React.Component { + private _header?: React.RefObject = React.createRef(); + private _colDropDisposer?: DragManager.DragDropDisposer; + private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; + private _sensitivity: number = 16; + private _dragRef: React.RefObject = React.createRef(); + + onPointerEnter = (e: React.PointerEvent): void => { + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { + this._header!.current!.className = "collectionSchema-col-wrapper"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + this._header!.current!.className = "collectionSchema-col-wrapper"; + document.removeEventListener("pointermove", this.onDragMove, true); + !e.buttons && document.removeEventListener("pointermove", this.onPointerMove); + } + onDragMove = (e: PointerEvent): void => { + const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); + const before = x[0] < bounds[0]; + this._header!.current!.className = "collectionSchema-col-wrapper"; + if (before) this._header!.current!.className += " col-before"; + if (!before) this._header!.current!.className += " col-after"; + e.stopPropagation(); + } + + createColDropTarget = (ele: HTMLDivElement) => { + this._colDropDisposer?.(); + if (ele) { + this._colDropDisposer = DragManager.MakeDropTarget(ele, this.colDrop.bind(this)); + } + } + + colDrop = (e: Event, de: DragManager.DropEvent) => { + document.removeEventListener("pointermove", this.onDragMove, true); + const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); + const before = x[0] < bounds[0]; + const colDragData = de.complete.columnDragData; + if (colDragData) { + e.stopPropagation(); + this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns); + return true; + } + return false; + } + + onPointerMove = (e: PointerEvent) => { + const onRowMove = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + const dragData = new DragManager.ColumnDragData(this.props.columnValue); + DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y); + }; + const onRowUp = (): void => { + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + }; + if (e.buttons === 1) { + const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { + document.removeEventListener("pointermove", this.onPointerMove); + e.stopPropagation(); + + document.addEventListener("pointermove", onRowMove); + document.addEventListener("pointerup", onRowUp); + } + } + } + + onPointerUp = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + } + + @action + onPointerDown = (e: React.PointerEvent, ref: React.RefObject) => { + this._dragRef = ref; + const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); + if (!(e.target as any)?.tagName.includes("INPUT")) { + this._startDragPosition = { x: dx, y: dy }; + document.addEventListener("pointermove", this.onPointerMove); + } + } + + + render() { + const reference = React.createRef(); + + return ( +
+
+
this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}> + {this.props.columnRenderer} +
+
+
+ ); + } +} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx new file mode 100644 index 000000000..f48906ba5 --- /dev/null +++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx @@ -0,0 +1,147 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; +import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; +import { Doc } from "../../../../fields/Doc"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { Cast, FieldValue, StrCast } from "../../../../fields/Types"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { DragManager, dropActionType, SetupDrag } from "../../../util/DragManager"; +import { SnappingManager } from "../../../util/SnappingManager"; +import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; +import { ContextMenu } from "../../ContextMenu"; +import "./CollectionSchemaView.scss"; + +export interface MovableRowProps { + rowInfo: RowInfo; + ScreenToLocalTransform: () => Transform; + addDoc: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; + removeDoc: (doc: Doc | Doc[]) => boolean; + rowFocused: boolean; + textWrapRow: (doc: Doc) => void; + rowWrapped: boolean; + dropAction: string; + addDocTab: any; +} + +export class MovableRow extends React.Component { + private _header?: React.RefObject = React.createRef(); + private _rowDropDisposer?: DragManager.DragDropDisposer; + + // Event listeners are only necessary when the user is hovering over the table + // Create one when the mouse starts hovering... + onPointerEnter = (e: React.PointerEvent): void => { + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { + this._header!.current!.className = "collectionSchema-row-wrapper"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + // ... and delete it when the mouse leaves + onPointerLeave = (e: React.PointerEvent): void => { + this._header!.current!.className = "collectionSchema-row-wrapper"; + document.removeEventListener("pointermove", this.onDragMove, true); + } + // The method for the event listener, reorders columns when dragged to their new locations. + onDragMove = (e: PointerEvent): void => { + const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + const before = x[1] < bounds[1]; + this._header!.current!.className = "collectionSchema-row-wrapper"; + if (before) this._header!.current!.className += " row-above"; + if (!before) this._header!.current!.className += " row-below"; + e.stopPropagation(); + } + componentWillUnmount() { + + this._rowDropDisposer?.(); + } + // + createRowDropTarget = (ele: HTMLDivElement) => { + this._rowDropDisposer?.(); + if (ele) { + this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); + } + } + // Controls what hppens when a row is dragged and dropped + rowDrop = (e: Event, de: DragManager.DropEvent) => { + this.onPointerLeave(e as any); + const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc)); + if (!rowDoc) return false; + + const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + const before = x[1] < bounds[1]; + + const docDragData = de.complete.docDragData; + if (docDragData) { + e.stopPropagation(); + if (docDragData.draggedDocuments[0] === rowDoc) return true; + const addDocument = (doc: Doc | Doc[]) => this.props.addDoc(doc, rowDoc, before); + const movedDocs = docDragData.draggedDocuments; + return (docDragData.dropAction || docDragData.userDropAction) ? + docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false) + : (docDragData.moveDocument) ? + movedDocs.reduce((added: boolean, d) => docDragData.moveDocument?.(d, rowDoc, addDocument) || added, false) + : docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false); + } + return false; + } + + onRowContextMenu = (e: React.MouseEvent): void => { + const description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; + ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" }); + } + + @undoBatch + @action + move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => { + const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection); + return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); + } + + @action + onKeyDown = (e: React.KeyboardEvent) => { + console.log("yes"); + if (e.key === "Backspace" || e.key === "Delete") { + undoBatch(() => this.props.removeDoc(this.props.rowInfo.original)); + } + } + + render() { + const { children = null, rowInfo } = this.props; + + if (!rowInfo) { + return {children}; + } + + const { original } = rowInfo; + const doc = FieldValue(Cast(original, Doc)); + + if (!doc) return (null); + + const reference = React.createRef(); + const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType); + + let className = "collectionSchema-row"; + if (this.props.rowFocused) className += " row-focused"; + if (this.props.rowWrapped) className += " row-wrapped"; + + return ( +
+
+ +
+
this.props.removeDoc(this.props.rowInfo.original))}>
+
+
this.props.addDocTab(this.props.rowInfo.original, "add:right")}>
+
+ {children} +
+
+
+ ); + } +} \ No newline at end of file diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss new file mode 100644 index 000000000..b57fee0e4 --- /dev/null +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -0,0 +1,552 @@ +@import "../../globalCssVariables"; +.collectionSchemaView-container { + border-width: $COLLECTION_BORDER_WIDTH; + border-color: $intermediate-color; + 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: $light-color; + // } +} + +.collectionSchemaView-searchContainer { + border-width: $COLLECTION_BORDER_WIDTH; + border-color: $intermediate-color; + 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: $light-color; + // } +} + +.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: solid lightgray; + border-width: 0 1px; + border-bottom: 2px solid lightgray; + } + } + .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 { + 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; +} + +.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: $main-accent; + 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-color-secondary; + } + &.active { + font-weight: bold; + border: 2px solid $light-color-secondary; + } + svg { + color: gray; + margin-right: 5px; + width: 10px; + } + } + .keys-dropdown { + position: relative; + //width: 100%; + background-color: white; + input { + border: 2px solid $light-color-secondary; + padding: 3px; + height: 28px; + font-weight: bold; + letter-spacing: "2px"; + text-transform: "uppercase"; + &:focus { + font-weight: normal; + } + } + .keys-options-wrapper { + width: 100%; + max-height: 150px; + overflow-y: scroll; + position: absolute; + top: 28px; + box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); + background-color: white; + .key-option { + background-color: white; + border: 1px solid lightgray; + padding: 2px 3px; + &:not(:first-child) { + border-top: 0; + } + &:hover { + background-color: $light-color-secondary; + } + } + } + } + .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; + } + } + } +} + +.collectionSchema-row { + height: 100%; + background-color: white; + &.row-focused .rt-td { + background-color: #bfffc0; //$light-color-secondary; + } + &.row-wrapped { + .rt-td { + white-space: normal; + } + } + .row-dragger { + display: flex; + justify-content: space-around; + //flex: 50 0 auto; + width: 0; + max-width: 50px; + //height: 100%; + min-height: 30px; + align-items: center; + color: lightgray; + background-color: white; + transition: color 0.1s ease; + .row-option { + // padding: 5px; + cursor: pointer; + position: absolute; + transition: color 0.1s ease; + display: flex; + flex-direction: column; + justify-content: center; + z-index: 2; + &:hover { + color: gray; + } + } + } + .collectionSchema-row-wrapper { + &.row-above { + border-top: 1px solid red; + } + &.row-below { + border-bottom: 1px solid red; + } + &.row-inside { + border: 1px solid red; + } + .row-dragging { + background-color: blue; + } + } +} + +.collectionSchemaView-cellContainer { + width: 100%; + height: unset; +} + +.collectionSchemaView-cellWrapper { + height: 100%; + padding: 4px; + text-align: left; + padding-left: 19px; + position: relative; + &:focus { + outline: none; + } + &.editing { + padding: 0; + input { + outline: 0; + border: none; + background-color: rgb(255, 217, 217); + width: 100%; + height: 100%; + padding: 2px 3px; + min-height: 26px; + } + } + &.focused { + &.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; +} \ No newline at end of file diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx new file mode 100644 index 000000000..ef28f75c8 --- /dev/null +++ b/src/client/views/collections/collectionSchema/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 '../../globalCssVariables.scss'; +import { ContextMenu } from "../../ContextMenu"; +import { ContextMenuProps } from "../../ContextMenuItem"; +import '../../../views/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 diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx new file mode 100644 index 000000000..0d5c9e077 --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -0,0 +1,601 @@ +import React = require("react"); +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; +import "react-table/react-table.css"; +import { DateField } from "../../../../fields/DateField"; +import { AclPrivate, AclReadonly, DataSym, Doc, DocListCast, Field, Opt } from "../../../../fields/Doc"; +import { Id } from "../../../../fields/FieldSymbols"; +import { List } from "../../../../fields/List"; +import { listSpec } from "../../../../fields/Schema"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; +import { ImageField } from "../../../../fields/URLField"; +import { GetEffectiveAcl } from "../../../../fields/util"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../../Utils"; +import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { CompileScript, Transformer, ts } from "../../../util/Scripting"; +import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../globalCssVariables.scss'; +import { ContextMenu } from "../../ContextMenu"; +import '../../../views/DocumentDecorations.scss'; +import { DocumentView } from "../../nodes/DocumentView"; +import { DefaultStyleProvider } from "../../StyleProvider"; +import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; +import { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; +import { MovableColumn } from "./CollectionSchemaMovableColumn"; +import { MovableRow } from "./CollectionSchemaMovableRow"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "../CollectionView"; + + +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] +]); + +export interface SchemaTableProps { + Document: Doc; // child doc + dataDoc?: Doc; + PanelHeight: () => number; + PanelWidth: () => number; + childDocs?: Doc[]; + CollectionView: Opt; + ContainingCollectionView: Opt; + ContainingCollectionDoc: Opt; + fieldKey: string; + renderDepth: number; + deleteDocument?: (document: Doc | Doc[]) => boolean; + addDocument?: (document: Doc | Doc[]) => boolean; + moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; + ScreenToLocalTransform: () => Transform; + active: (outsideReaction: boolean | undefined) => boolean; + onDrop: (e: React.DragEvent, options: DocumentOptions, completed?: (() => void) | undefined) => void; + addDocTab: (document: Doc, where: string) => boolean; + pinToPres: (document: Doc) => void; + isSelected: (outsideReaction?: boolean) => boolean; + isFocused: (document: Doc, outsideReaction: boolean) => boolean; + setFocused: (document: Doc) => void; + setPreviewDoc: (document: Opt) => void; + columns: SchemaHeaderField[]; + documentKeys: any[]; + headerIsEditing: boolean; + openHeader: (column: any, screenx: number, screeny: number) => void; + onClick: (e: React.MouseEvent) => void; + onPointerDown: (e: React.PointerEvent) => void; + onResizedChange: (newResized: Resize[], event: any) => void; + setColumns: (columns: SchemaHeaderField[]) => void; + reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => void; + changeColumns: (oldKey: string, newKey: string, addNew: boolean) => void; + setHeaderIsEditing: (isEditing: boolean) => void; + changeColumnSort: (columnField: SchemaHeaderField, descending: boolean | undefined) => void; +} + +@observer +export class SchemaTable extends React.Component { + @observable _cellIsEditing: boolean = false; + @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; + @observable _openCollections: Set = new Set; + + @observable _showDoc: Doc | undefined; + @observable _showDataDoc: any = ""; + @observable _showDocPos: number[] = []; + + @observable _showTitleDropdown: 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 childDocs() { + if (this.props.childDocs) return this.props.childDocs; + + const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + return DocListCast(doc[this.props.fieldKey]); + } + set childDocs(docs: Doc[]) { + const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + doc[this.props.fieldKey] = new List(docs); + } + + @computed get textWrappedRows() { + return Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + } + set textWrappedRows(textWrappedRows: string[]) { + this.props.Document.textwrappedSchemaRows = new List(textWrappedRows); + } + + @computed get resized(): { id: string, value: number }[] { + return this.props.columns.reduce((resized, shf) => { + (shf.width > -1) && resized.push({ id: shf.heading, value: shf.width }); + return resized; + }, [] as { id: string, value: number }[]); + } + @computed get sorted(): SortingRule[] { + return this.props.columns.reduce((sorted, shf) => { + shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc }); + return sorted; + }, [] as SortingRule[]); + } + + @action + changeSorting = (col: any) => { + this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true); + } + + @action + changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown + + @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } + @computed get tableColumns(): Column[] { + const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); + const columns: Column[] = []; + const tableIsFocused = this.props.isFocused(this.props.Document, false); + const focusedRow = this._focusedCell.row; + const focusedCol = this._focusedCell.col; + const isEditable = !this.props.headerIsEditing; + + columns.push({ + expander: true, Header: "", width: 58, + Expander: (rowInfo) => { + return rowInfo.original.type !== DocumentType.COL ? (null) : +
(this._openCollections[rowInfo.isExpanded ? "delete" : "add"])(rowInfo.viewIndex))}> + +
; + } + }); + columns.push(...this.props.columns.map(col => { + const icon: IconProp = this.getColumnType(col) === ColumnType.Number ? "hashtag" : this.getColumnType(col) === ColumnType.String ? "font" : + this.getColumnType(col) === ColumnType.Boolean ? "check-square" : this.getColumnType(col) === ColumnType.Doc ? "file" : + this.getColumnType(col) === ColumnType.Image ? "image" : this.getColumnType(col) === ColumnType.List ? "list-ul" : + this.getColumnType(col) === ColumnType.Date ? "calendar" : "align-justify"; + + const keysDropdown = c.heading)} + canAddNew={true} + addNew={false} + onSelect={this.props.changeColumns} + setIsEditing={this.props.setHeaderIsEditing} + docs={this.props.childDocs} + Document={this.props.Document} + dataDoc={this.props.dataDoc} + fieldKey={this.props.fieldKey} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + ContainingCollectionView={this.props.ContainingCollectionView} + active={this.props.active} + openHeader={this.props.openHeader} + icon={icon} + col={col} + // try commenting this out + width={"100%"} + />; + + const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; + const header =
+ {keysDropdown} +
this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> + +
+
; + + return { + Header: , + accessor: (doc: Doc) => doc ? Field.toString(doc[col.heading] as Field) : 0, + id: col.heading, + Cell: (rowProps: CellInfo) => { + const rowIndex = rowProps.index; + const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); + const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; + + const props: CellProps = { + row: rowIndex, + col: columnIndex, + rowProps: rowProps, + isFocused: isFocused, + changeFocusedCellByIndex: this.changeFocusedCellByIndex, + CollectionView: this.props.CollectionView, + ContainingCollection: this.props.ContainingCollectionView, + Document: this.props.Document, + fieldKey: this.props.fieldKey, + renderDepth: this.props.renderDepth, + addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, + moveDocument: this.props.moveDocument, + setIsEditing: this.setCellIsEditing, + isEditable: isEditable, + setPreviewDoc: this.props.setPreviewDoc, + setComputed: this.setComputed, + getField: this.getField, + showDoc: this.showDoc, + }; + + + switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) { + case ColumnType.Number: return ; + case ColumnType.String: return ; + case ColumnType.Boolean: return ; + case ColumnType.Doc: return ; + case ColumnType.Image: return ; + case ColumnType.List: return ; + case ColumnType.Date: return ; + default: + return ; + } + }, + minWidth: 200, + }; + })); + columns.push({ + Header: , + accessor: (doc: Doc) => 0, + id: "add", + Cell: (rowProps: CellInfo) => { + const rowIndex = rowProps.index; + const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); + const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; + return ; + }, + width: 28, + resizable: false + }); + return columns; + } + + + constructor(props: SchemaTableProps) { + super(props); + if (this.props.Document._schemaHeaders === undefined) { + this.props.Document._schemaHeaders = new List([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb", ColumnType.Date), + new SchemaHeaderField("text", "#f1efeb", ColumnType.String), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb", ColumnType.Doc)]); + } + } + + componentDidMount() { + document.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); + } + + tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + const tableDoc = this.props.Document[DataSym]; + const effectiveAcl = GetEffectiveAcl(tableDoc); + + if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) { + doc.context = this.props.Document; + tableDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + } + return false; + } + + private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { + return !rowInfo ? {} : { + ScreenToLocalTransform: this.props.ScreenToLocalTransform, + addDoc: this.tableAddDoc, + removeDoc: this.props.deleteDocument, + rowInfo, + rowFocused: !this.props.headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document, true), + textWrapRow: this.toggleTextWrapRow, + rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1, + dropAction: StrCast(this.props.Document.childDropAction), + addDocTab: this.props.addDocTab + }; + } + + private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => { + if (!rowInfo || column) return {}; + + const row = rowInfo.index; + //@ts-ignore + const col = this.columns.map(c => c.heading).indexOf(column!.id); + const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true); + // TODO: editing border doesn't work :( + return { + style: { border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" } + }; + } + + @action setCellIsEditing = (isEditing: boolean) => this._cellIsEditing = isEditing; + + @action + onKeyDown = (e: KeyboardEvent): void => { + if (!this._cellIsEditing && !this.props.headerIsEditing && this.props.isFocused(this.props.Document, true)) {// && this.props.isSelected(true)) { + const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; + this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); + + if (direction) { + const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); + pdoc && this.props.setPreviewDoc(pdoc); + e.stopPropagation(); + } + } else if (e.keyCode === 27) { + this.props.setPreviewDoc(undefined); + e.stopPropagation(); // stopPropagation for left/right arrows + } + } + + changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => { + switch (direction) { + case "tab": return { row: (curRow + 1 === this.childDocs.length ? 0 : curRow + 1), col: curCol + 1 === this.props.columns.length ? 0 : curCol + 1 }; + case "right": return { row: curRow, col: curCol + 1 === this.props.columns.length ? curCol : curCol + 1 }; + case "left": return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 }; + case "up": return { row: curRow === 0 ? curRow : curRow - 1, col: curCol }; + case "down": return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol }; + } + return this._focusedCell; + } + + @action + changeFocusedCellByIndex = (row: number, col: number): void => { + if (this._focusedCell.row !== row || this._focusedCell.col !== col) { + this._focusedCell = { row: row, col: col }; + } + this.props.setFocused(this.props.Document); + } + + @undoBatch + createRow = action(() => { + this.props.addDocument?.(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); + this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col }; + }); + + @undoBatch + @action + createColumn = () => { + let index = 0; + let found = this.props.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; + while (found) { + index++; + found = this.props.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; + } + this.props.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb")); + } + + @action + getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => { + if (doc && field && column.type === ColumnType.Any) { + const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)]; + if (val instanceof ImageField) return ColumnType.Image; + if (val instanceof Doc) return ColumnType.Doc; + if (val instanceof DateField) return ColumnType.Date; + if (val instanceof List) return ColumnType.List; + } + if (column.type && column.type !== 0) { + return column.type; + } + if (columnTypes.get(column.heading)) { + return column.type = columnTypes.get(column.heading)!; + } + return column.type = ColumnType.Any; + } + + @undoBatch + @action + toggleTextwrap = async () => { + const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.props.Document.textwrappedSchemaRows = new List([]); + } else { + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.props.Document.textwrappedSchemaRows = new List(allRows); + } + } + + @action + toggleTextWrapRow = (doc: Doc): void => { + const textWrapped = this.textWrappedRows; + const index = textWrapped.findIndex(id => doc[Id] === id); + + index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]); + + this.textWrappedRows = textWrapped; + } + + @computed + get reactTable() { + const children = this.childDocs; + const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false); + const expanded: { [name: string]: any } = {}; + Array.from(this._openCollections.keys()).map(col => expanded[col.toString()] = true); + const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( + + return (row.original.type !== DocumentType.COL) ? (null) : +
} + + />; + } + + onContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); + } + + getField = (row: number, col?: number) => { + const docs = this.childDocs; + + row = row % docs.length; + while (row < 0) row += docs.length; + const columns = this.props.columns; + const doc = docs[row]; + if (col === undefined) { + return doc; + } + if (col >= 0 && col < columns.length) { + const column = this.props.columns[col].heading; + return doc[column]; + } + return undefined; + } + + createTransformer = (row: number, col: number): Transformer => { + const self = this; + const captures: { [name: string]: Field } = {}; + + const transformer: ts.TransformerFactory = context => { + return root => { + function visit(node: ts.Node) { + node = ts.visitEachChild(node, visit, context); + if (ts.isIdentifier(node)) { + const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; + const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; + if (isntPropAccess && isntPropAssign) { + if (node.text === "$r") { + return ts.createNumericLiteral(row.toString()); + } else if (node.text === "$c") { + return ts.createNumericLiteral(col.toString()); + } else if (node.text === "$") { + if (ts.isCallExpression(node.parent)) { + // captures.doc = self.props.Document; + // captures.key = self.props.fieldKey; + } + } + } + } + + return node; + } + return ts.visitNode(root, visit); + }; + }; + + // const getVars = () => { + // return { capturedVariables: captures }; + // }; + + return { transformer, /*getVars*/ }; + } + + setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => { + script = + `const $ = (row:number, col?:number) => { + const rval = (doc as any)[key][row + ${row}]; + return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading]; + } + return ${script}`; + const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); + if (compiled.compiled) { + doc[field] = new ComputedField(compiled); + return true; + } + return false; + } + + @action + showDoc = (doc: Doc | undefined, dataDoc?: Doc, screenX?: number, screenY?: number) => { + this._showDoc = doc; + if (dataDoc && screenX && screenY) { + this._showDocPos = this.props.ScreenToLocalTransform().transformPoint(screenX, screenY); + } + } + + onOpenClick = () => { + this._showDoc && this.props.addDocTab(this._showDoc, "add:right"); + } + + getPreviewTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(- this.borderWidth - 4 - this.tableWidth, - this.borderWidth); + } + + render() { + const preview = ""; + return
this.props.active(true) && e.stopPropagation()} + onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > + {this.reactTable} + {this.props.Document._chromeHidden ? undefined :
+ new
} + {!this._showDoc ? (null) : +
+ 150} + PanelHeight={() => 150} + ScreenToLocalTransform={this.getPreviewTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + whenChildContentsActiveChanged={emptyFunction} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse}> + +
} +
; + } +} \ No newline at end of file diff --git a/src/client/views/collections/schemaView/CollectionSchemaCells.tsx b/src/client/views/collections/schemaView/CollectionSchemaCells.tsx deleted file mode 100644 index f75179cea..000000000 --- a/src/client/views/collections/schemaView/CollectionSchemaCells.tsx +++ /dev/null @@ -1,585 +0,0 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import { CellInfo } from "react-table"; -import "react-table/react-table.css"; -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, NumCast, StrCast } from "../../../../fields/Types"; -import { ImageField } from "../../../../fields/URLField"; -import { Utils, emptyFunction } 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 { MAX_ROW_HEIGHT } from '../../globalCssVariables.scss'; -import { DocumentIconContainer } from "../../nodes/DocumentIcon"; -import { OverlayView } from "../../OverlayView"; -import "./CollectionSchemaView.scss"; -import { CollectionView } from "../CollectionView"; -const path = require('path'); - -// intialize cell properties -export interface CellProps { - row: number; - col: number; - rowProps: CellInfo; - CollectionView: Opt; - ContainingCollection: Opt; - Document: Doc; - fieldKey: string; - renderDepth: number; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; - moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, - addDocument: (document: Doc | Doc[]) => boolean) => boolean; - isFocused: boolean; - changeFocusedCellByIndex: (row: number, col: number) => void; - setIsEditing: (isEditing: boolean) => void; - isEditable: boolean; - setPreviewDoc: (doc: Doc) => void; - setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean; - getField: (row: number, col?: number) => void; - showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void; -} - -@observer -export class CollectionSchemaCell extends React.Component { - public static resolvedFieldKey(column: string, rowDoc: Doc) { - const fieldKey = column; - 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)); - if (matchedKeys.length) return matchedKeys[0]; - } - return fieldKey; - } - @observable protected _isEditing: boolean = false; - protected _focusRef = React.createRef(); - protected _rowDoc = this.props.rowProps.original; - protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); - protected _dropDisposer?: DragManager.DragDropDisposer; - @observable contents: string = ""; - - componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } - componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } - - @action - onKeyDown = (e: KeyboardEvent): void => { - if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) { - document.removeEventListener("keydown", this.onKeyDown); - this._isEditing = true; - this.props.setIsEditing(true); - } - } - - @action - isEditingCallback = (isEditing: boolean): void => { - 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); - } - - @action - onPointerDown = async (e: React.PointerEvent): Promise => { - this.onItemDown(e); - this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - this.props.setPreviewDoc(this.props.rowProps.original); - - let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { - try { - new URL(url); - const temp = window.open(url)!; - temp.blur(); - window.focus(); - } 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) => { - const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) }); - if (!res.success) return false; - doc[this.renderFieldKey] = res.result; - return true; - } - - private drop = (e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData) { - if (de.complete.docDragData.draggedDocuments.length === 1) { - this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; - } - else { - 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) => { - this._dropDisposer?.(); - 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"; - - results.push({contents?.slice(0, positions[0])}); - positions.forEach((num, cur) => { - results.push({contents?.slice(num, num + length)}); - let end = 0; - cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; - results.push({contents?.slice(num + length, end)}); - } - ); - return results; - } - return {contents ? contents?.valueOf() : "undefined"}; - } - - @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); } - onItemDown = async (e: React.PointerEvent) => { - if (this.props.Document._searchDoc) { - const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); - const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); - DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext, - undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); - } - } - renderCellWithType(type: string | undefined) { - const dragRef: React.RefObject = React.createRef(); - - const fieldKey = this.renderFieldKey; - const field = this._rowDoc[fieldKey]; - - const onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { - dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; - } - }; - const onPointerLeave = (e: React.PointerEvent): void => { - dragRef.current!.className = "collectionSchemaView-cellContainer"; - }; - - let contents = Field.toString(field as Field); - contents = contents === "" ? "--" : contents; - - 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() !== "") { - 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; - if (start !== -1) { - positions.push(start); - } - while (start < contents?.length && start !== -1) { - term = term.slice(start + search.length + 1); - tally += start + search.length + 1; - start = term.indexOf(search); - positions.push(tally + start); - } - if (positions.length > 1) { - positions.pop(); - } - } - const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; - return ( -
this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> -
-
- {!this.props.Document._searchDoc ? - { - 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) : ""; - }} - SetValue={action((value: string) => { - // sets what is displayed after the user makes an input - let retVal = false; - 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); - } else { - // check if the input is a number - let inputIsNum = true; - for (let s of value) { - if (isNaN(parseInt(s)) && !(s == ".") && !(s == ",")) { - inputIsNum = false; - } - } - // check if the input is a boolean - let 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 - // 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); - } - let inputAsString = '"'; - // escape any quotes in the string - for (const i of valueSansQuotes) { - if (i == '"') { - inputAsString += '\\"'; - } else { - inputAsString += i; - } - } - // 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 changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length - 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("=")) { - //TODO: make accept numbers - 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 = ""; - for (let s of inputscript) { - 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 changeMade = value.length !== value.length || 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 changeMade = value.length !== value.length || value.length - 2 !== value.length - script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); - } - } - if (retVal) { - this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' - this.props.setIsEditing(false); - } - return retVal; - })} - OnFillDown={async (value: string) => { - 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) - } -
-
-
- ); - } - - render() { return this.renderCellWithType(undefined); } -} - -@observer -export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } - -@observer -export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } - -@observer -export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } - -@observer -export class CollectionSchemaDateCell extends CollectionSchemaCell { - @computed get _date(): Opt { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; } - - @action - handleChange = (date: any) => { - // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); - // if (script.compiled) { - // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); - // } else { - // ^ 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 ? {this._date ? Field.toString(this._date as Field) : "--"} : - 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)); } - - @action - onSetValue = (value: string) => { - this._doc && (Doc.GetProto(this._doc).title = value); - - const script = CompileScript(value, { - addReturn: true, - typecheck: true, - transformer: DocumentIconContainer.getTransformer() - }); - - const results = script.compiled && script.run(); - if (results && results.success) { - this._rowDoc[this.renderFieldKey] = results.result; - return true; - } - return false; - } - - componentWillUnmount() { this.onBlur(); } - - onBlur = () => { this._overlayDisposer?.(); }; - onFocus = () => { - this.onBlur(); - this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); - } - - @action - isEditingCallback = (isEditing: boolean): void => { - 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() { - return !this._doc ? this.renderCellWithType("document") : -
-
- StrCast(this._doc?.title)} - SetValue={action((value: string) => { - this.onSetValue(value); - return true; - })} - /> -
-
this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> - -
-
; - } -} - -@observer -export class CollectionSchemaImageCell extends CollectionSchemaCell { - - choosePath(url: URL) { - if (url.protocol === "data") return url.href; - if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here - - const ext = path.extname(url.href); - return url.href.replace(ext, "_o" + path.extname(url.href)); - } - - 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 paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; - const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - - const aspect = Doc.NativeAspect(this._rowDoc); - let width = Math.min(75, this.props.rowProps.width); - const height = Math.min(75, width / aspect); - width = height * aspect; - - const reference = React.createRef(); - return
-
- -
-
; - } -} - - -@observer -export class CollectionSchemaListCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - - @computed get _field() { return this._rowDoc[this.renderFieldKey]; } - @computed get _optionsList() { return this._field as List; } - @observable private _opened = false; - @observable private _text = "select an item"; - @observable private _selectedNum = 0; - - @action - onSetValue = (value: string) => { - // change if its a document - this._optionsList[this._selectedNum] = this._text = value; - - (this._field as List).splice(this._selectedNum, 1, value); - } - - @action - onSelected = (element: string, index: number) => { - this._text = element; - this._selectedNum = index; - } - - onFocus = () => { - this._overlayDisposer?.(); - this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); - } - - render() { - const link = false; - const reference = React.createRef(); - - if (this._optionsList?.length) { - const options = !this._opened ? (null) : -
- {this._optionsList.map((element, index) => { - const val = Field.toString(element); - return
this.onSelected(StrCast(element), index)} > - {val} -
; - })} -
; - - const plainText =
{this._text}
; - const textarea =
- this._text} - SetValue={action((value: string) => { - // add special for params - this.onSetValue(value); - return true; - })} - /> -
; - - //☰ - return ( -
-
-
- -
{link ? plainText : textarea}
-
- {options} -
-
- ); - } - return this.renderCellWithType("list"); - } -} - - -@observer -export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } - - render() { - const reference = React.createRef(); - return ( -
- this._rowDoc[this.renderFieldKey] = e.target.checked} /> -
- ); - } -} - - -@observer -export class CollectionSchemaButtons extends CollectionSchemaCell { - render() { - return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <> : -
- - -
; - } -} \ No newline at end of file diff --git a/src/client/views/collections/schemaView/CollectionSchemaHeaders.tsx b/src/client/views/collections/schemaView/CollectionSchemaHeaders.tsx deleted file mode 100644 index b2115b22e..000000000 --- a/src/client/views/collections/schemaView/CollectionSchemaHeaders.tsx +++ /dev/null @@ -1,518 +0,0 @@ -import React = require("react"); -import { IconProp, library } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } 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 { SearchBox } from "../../search/SearchBox"; -import { ColumnType } from "./CollectionSchemaView"; -import "./CollectionSchemaView.scss"; -import { CollectionView } from "../CollectionView"; - -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 { - render() { - return ( - - ); - } -} - - -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 { - @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 ( -
- -
-
this.changeColumnType(ColumnType.Any)}> - - Any -
-
this.changeColumnType(ColumnType.Number)}> - - Number -
-
this.changeColumnType(ColumnType.String)}> - - Text -
-
this.changeColumnType(ColumnType.Boolean)}> - - Checkbox -
-
this.changeColumnType(ColumnType.List)}> - - List -
-
this.changeColumnType(ColumnType.Doc)}> - - Document -
-
this.changeColumnType(ColumnType.Image)}> - - Image -
-
this.changeColumnType(ColumnType.Date)}> - - Date -
-
-
- ); - } - - renderSorting = () => { - const sort = this.props.columnField.desc; - return ( -
- -
-
this.changeColumnSort(true)}> - - Sort descending -
-
this.changeColumnSort(false)}> - - Sort ascending -
-
this.changeColumnSort(undefined)}> - - Clear sorting -
-
-
- ); - } - - 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 ( -
- -
-
this.changeColumnColor(pink!)}>
-
this.changeColumnColor(purple!)}>
-
this.changeColumnColor(blue!)}>
-
this.changeColumnColor(yellow!)}>
-
this.changeColumnColor(red!)}>
-
this.changeColumnColor(gray)}>
-
-
- ); - } - - renderContent = () => { - return ( -
- {this.props.onlyShowOptions ? <> : - <> - {this.renderTypes()} - {this.renderSorting()} - {this.renderColors()} -
- -
- - } -
- ); - } - - render() { - return ( -
- -
this.toggleIsOpen()}>{this.props.menuButtonContent}
- -
- ); - } -} - - -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; - active?: (outsideReaction?: boolean) => boolean; - openHeader: (column: any, screenx: number, screeny: number) => void; - col: SchemaHeaderField; - icon: IconProp; -} -@observer -export class KeysDropdown extends React.Component { - @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 = 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") { - 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(); - [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.UserDoc().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)); - } - @action - renderOptions = (): JSX.Element[] | JSX.Element => { - if (!this._isOpen) { - this.defaultMenuHeight = 0; - return <>; - } - const options = this.showKeys.map(key => { - return
{ - e.stopPropagation(); - }} - onClick={() => { - this.onSelect(key); - this.setSearchTerm(""); - }}>{key}
; - }); - - // 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(
{ this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> - Create "{this._searchTerm}" key
); - } - } - - 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; - } - - docSafe: Doc[] = []; - - @action - renderFilterOptions = (): JSX.Element[] | JSX.Element => { - if (!this._isOpen || !this.props.dataDoc) { - this.defaultMenuHeight = 0; - return <>; - } - const keyOptions: string[] = []; - const colpos = this._searchTerm.indexOf(":"); - const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); - if (this.docSafe.length === 0) { - this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); - } - const docs = this.docSafe; - docs.forEach((doc) => { - const key = StrCast(doc[this._key]); - if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { - keyOptions.push(key); - } - }); - - 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"; - } - 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(":")[0] === key); - const fields = ind === -1 ? undefined : filters[ind].split(":"); - bool = fields ? fields[1] === "check" : false; - } - return
- e.stopPropagation()} - onClick={e => e.stopPropagation()} - onChange={(e) => { - e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove"); - e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log(""); - e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter(); - e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove"); - }} - checked={bool} - /> - - {key} - - -
; - }); - 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[] = []; - if (this.docSafe.length === 0 && this.props.dataDoc) { - this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); - } - const docs = this.docSafe; - docs.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 ( -
- { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> - - {/* { - runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) - }} /> */} - -
- this.onChange(e.target.value)} - onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} - onFocus={this.onFocus} > -
- -
- {!this._isOpen ? (null) :
- {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()} -
} -
-
- ); - } -} diff --git a/src/client/views/collections/schemaView/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/schemaView/CollectionSchemaMovableColumn.tsx deleted file mode 100644 index 456c38c68..000000000 --- a/src/client/views/collections/schemaView/CollectionSchemaMovableColumn.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action } from "mobx"; -import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; -import { Doc } from "../../../../fields/Doc"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { Cast, FieldValue, StrCast } from "../../../../fields/Types"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { DragManager, dropActionType, SetupDrag } from "../../../util/DragManager"; -import { SnappingManager } from "../../../util/SnappingManager"; -import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; -import { ContextMenu } from "../../ContextMenu"; -import "./CollectionSchemaView.scss"; - -export interface MovableColumnProps { - columnRenderer: TableCellRenderer; - columnValue: SchemaHeaderField; - allColumns: SchemaHeaderField[]; - reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void; - ScreenToLocalTransform: () => Transform; -} -export class MovableColumn extends React.Component { - private _header?: React.RefObject = React.createRef(); - private _colDropDisposer?: DragManager.DragDropDisposer; - private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; - private _sensitivity: number = 16; - private _dragRef: React.RefObject = React.createRef(); - - onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && SnappingManager.GetIsDragging()) { - this._header!.current!.className = "collectionSchema-col-wrapper"; - document.addEventListener("pointermove", this.onDragMove, true); - } - } - onPointerLeave = (e: React.PointerEvent): void => { - this._header!.current!.className = "collectionSchema-col-wrapper"; - document.removeEventListener("pointermove", this.onDragMove, true); - !e.buttons && document.removeEventListener("pointermove", this.onPointerMove); - } - onDragMove = (e: PointerEvent): void => { - const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - const rect = this._header!.current!.getBoundingClientRect(); - const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); - const before = x[0] < bounds[0]; - this._header!.current!.className = "collectionSchema-col-wrapper"; - if (before) this._header!.current!.className += " col-before"; - if (!before) this._header!.current!.className += " col-after"; - e.stopPropagation(); - } - - createColDropTarget = (ele: HTMLDivElement) => { - this._colDropDisposer?.(); - if (ele) { - this._colDropDisposer = DragManager.MakeDropTarget(ele, this.colDrop.bind(this)); - } - } - - colDrop = (e: Event, de: DragManager.DropEvent) => { - document.removeEventListener("pointermove", this.onDragMove, true); - const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - const rect = this._header!.current!.getBoundingClientRect(); - const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); - const before = x[0] < bounds[0]; - const colDragData = de.complete.columnDragData; - if (colDragData) { - e.stopPropagation(); - this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns); - return true; - } - return false; - } - - onPointerMove = (e: PointerEvent) => { - const onRowMove = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - - document.removeEventListener("pointermove", onRowMove); - document.removeEventListener('pointerup', onRowUp); - const dragData = new DragManager.ColumnDragData(this.props.columnValue); - DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y); - }; - const onRowUp = (): void => { - document.removeEventListener("pointermove", onRowMove); - document.removeEventListener('pointerup', onRowUp); - }; - if (e.buttons === 1) { - const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); - if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { - document.removeEventListener("pointermove", this.onPointerMove); - e.stopPropagation(); - - document.addEventListener("pointermove", onRowMove); - document.addEventListener("pointerup", onRowUp); - } - } - } - - onPointerUp = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.onPointerMove); - } - - @action - onPointerDown = (e: React.PointerEvent, ref: React.RefObject) => { - this._dragRef = ref; - const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); - if (!(e.target as any)?.tagName.includes("INPUT")) { - this._startDragPosition = { x: dx, y: dy }; - document.addEventListener("pointermove", this.onPointerMove); - } - } - - - render() { - const reference = React.createRef(); - - return ( -
-
-
this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}> - {this.props.columnRenderer} -
-
-
- ); - } -} diff --git a/src/client/views/collections/schemaView/CollectionSchemaMovableRow.tsx b/src/client/views/collections/schemaView/CollectionSchemaMovableRow.tsx deleted file mode 100644 index f48906ba5..000000000 --- a/src/client/views/collections/schemaView/CollectionSchemaMovableRow.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action } from "mobx"; -import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; -import { Doc } from "../../../../fields/Doc"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { Cast, FieldValue, StrCast } from "../../../../fields/Types"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { DragManager, dropActionType, SetupDrag } from "../../../util/DragManager"; -import { SnappingManager } from "../../../util/SnappingManager"; -import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; -import { ContextMenu } from "../../ContextMenu"; -import "./CollectionSchemaView.scss"; - -export interface MovableRowProps { - rowInfo: RowInfo; - ScreenToLocalTransform: () => Transform; - addDoc: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; - removeDoc: (doc: Doc | Doc[]) => boolean; - rowFocused: boolean; - textWrapRow: (doc: Doc) => void; - rowWrapped: boolean; - dropAction: string; - addDocTab: any; -} - -export class MovableRow extends React.Component { - private _header?: React.RefObject = React.createRef(); - private _rowDropDisposer?: DragManager.DragDropDisposer; - - // Event listeners are only necessary when the user is hovering over the table - // Create one when the mouse starts hovering... - onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && SnappingManager.GetIsDragging()) { - this._header!.current!.className = "collectionSchema-row-wrapper"; - document.addEventListener("pointermove", this.onDragMove, true); - } - } - // ... and delete it when the mouse leaves - onPointerLeave = (e: React.PointerEvent): void => { - this._header!.current!.className = "collectionSchema-row-wrapper"; - document.removeEventListener("pointermove", this.onDragMove, true); - } - // The method for the event listener, reorders columns when dragged to their new locations. - onDragMove = (e: PointerEvent): void => { - const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - const rect = this._header!.current!.getBoundingClientRect(); - const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); - const before = x[1] < bounds[1]; - this._header!.current!.className = "collectionSchema-row-wrapper"; - if (before) this._header!.current!.className += " row-above"; - if (!before) this._header!.current!.className += " row-below"; - e.stopPropagation(); - } - componentWillUnmount() { - - this._rowDropDisposer?.(); - } - // - createRowDropTarget = (ele: HTMLDivElement) => { - this._rowDropDisposer?.(); - if (ele) { - this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); - } - } - // Controls what hppens when a row is dragged and dropped - rowDrop = (e: Event, de: DragManager.DropEvent) => { - this.onPointerLeave(e as any); - const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc)); - if (!rowDoc) return false; - - const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - const rect = this._header!.current!.getBoundingClientRect(); - const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); - const before = x[1] < bounds[1]; - - const docDragData = de.complete.docDragData; - if (docDragData) { - e.stopPropagation(); - if (docDragData.draggedDocuments[0] === rowDoc) return true; - const addDocument = (doc: Doc | Doc[]) => this.props.addDoc(doc, rowDoc, before); - const movedDocs = docDragData.draggedDocuments; - return (docDragData.dropAction || docDragData.userDropAction) ? - docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false) - : (docDragData.moveDocument) ? - movedDocs.reduce((added: boolean, d) => docDragData.moveDocument?.(d, rowDoc, addDocument) || added, false) - : docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false); - } - return false; - } - - onRowContextMenu = (e: React.MouseEvent): void => { - const description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; - ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" }); - } - - @undoBatch - @action - move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => { - const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection); - return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); - } - - @action - onKeyDown = (e: React.KeyboardEvent) => { - console.log("yes"); - if (e.key === "Backspace" || e.key === "Delete") { - undoBatch(() => this.props.removeDoc(this.props.rowInfo.original)); - } - } - - render() { - const { children = null, rowInfo } = this.props; - - if (!rowInfo) { - return {children}; - } - - const { original } = rowInfo; - const doc = FieldValue(Cast(original, Doc)); - - if (!doc) return (null); - - const reference = React.createRef(); - const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType); - - let className = "collectionSchema-row"; - if (this.props.rowFocused) className += " row-focused"; - if (this.props.rowWrapped) className += " row-wrapped"; - - return ( -
-
- -
-
this.props.removeDoc(this.props.rowInfo.original))}>
-
-
this.props.addDocTab(this.props.rowInfo.original, "add:right")}>
-
- {children} -
-
-
- ); - } -} \ No newline at end of file diff --git a/src/client/views/collections/schemaView/CollectionSchemaView.scss b/src/client/views/collections/schemaView/CollectionSchemaView.scss deleted file mode 100644 index b57fee0e4..000000000 --- a/src/client/views/collections/schemaView/CollectionSchemaView.scss +++ /dev/null @@ -1,552 +0,0 @@ -@import "../../globalCssVariables"; -.collectionSchemaView-container { - border-width: $COLLECTION_BORDER_WIDTH; - border-color: $intermediate-color; - 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: $light-color; - // } -} - -.collectionSchemaView-searchContainer { - border-width: $COLLECTION_BORDER_WIDTH; - border-color: $intermediate-color; - 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: $light-color; - // } -} - -.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: solid lightgray; - border-width: 0 1px; - border-bottom: 2px solid lightgray; - } - } - .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 { - 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; -} - -.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: $main-accent; - 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-color-secondary; - } - &.active { - font-weight: bold; - border: 2px solid $light-color-secondary; - } - svg { - color: gray; - margin-right: 5px; - width: 10px; - } - } - .keys-dropdown { - position: relative; - //width: 100%; - background-color: white; - input { - border: 2px solid $light-color-secondary; - padding: 3px; - height: 28px; - font-weight: bold; - letter-spacing: "2px"; - text-transform: "uppercase"; - &:focus { - font-weight: normal; - } - } - .keys-options-wrapper { - width: 100%; - max-height: 150px; - overflow-y: scroll; - position: absolute; - top: 28px; - box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); - background-color: white; - .key-option { - background-color: white; - border: 1px solid lightgray; - padding: 2px 3px; - &:not(:first-child) { - border-top: 0; - } - &:hover { - background-color: $light-color-secondary; - } - } - } - } - .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; - } - } - } -} - -.collectionSchema-row { - height: 100%; - background-color: white; - &.row-focused .rt-td { - background-color: #bfffc0; //$light-color-secondary; - } - &.row-wrapped { - .rt-td { - white-space: normal; - } - } - .row-dragger { - display: flex; - justify-content: space-around; - //flex: 50 0 auto; - width: 0; - max-width: 50px; - //height: 100%; - min-height: 30px; - align-items: center; - color: lightgray; - background-color: white; - transition: color 0.1s ease; - .row-option { - // padding: 5px; - cursor: pointer; - position: absolute; - transition: color 0.1s ease; - display: flex; - flex-direction: column; - justify-content: center; - z-index: 2; - &:hover { - color: gray; - } - } - } - .collectionSchema-row-wrapper { - &.row-above { - border-top: 1px solid red; - } - &.row-below { - border-bottom: 1px solid red; - } - &.row-inside { - border: 1px solid red; - } - .row-dragging { - background-color: blue; - } - } -} - -.collectionSchemaView-cellContainer { - width: 100%; - height: unset; -} - -.collectionSchemaView-cellWrapper { - height: 100%; - padding: 4px; - text-align: left; - padding-left: 19px; - position: relative; - &:focus { - outline: none; - } - &.editing { - padding: 0; - input { - outline: 0; - border: none; - background-color: rgb(255, 217, 217); - width: 100%; - height: 100%; - padding: 2px 3px; - min-height: 26px; - } - } - &.focused { - &.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; -} \ No newline at end of file diff --git a/src/client/views/collections/schemaView/CollectionSchemaView.tsx b/src/client/views/collections/schemaView/CollectionSchemaView.tsx deleted file mode 100644 index ef28f75c8..000000000 --- a/src/client/views/collections/schemaView/CollectionSchemaView.tsx +++ /dev/null @@ -1,575 +0,0 @@ -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 '../../globalCssVariables.scss'; -import { ContextMenu } from "../../ContextMenu"; -import { ContextMenuProps } from "../../ContextMenuItem"; -import '../../../views/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 diff --git a/src/client/views/collections/schemaView/SchemaTable.tsx b/src/client/views/collections/schemaView/SchemaTable.tsx deleted file mode 100644 index 0d5c9e077..000000000 --- a/src/client/views/collections/schemaView/SchemaTable.tsx +++ /dev/null @@ -1,601 +0,0 @@ -import React = require("react"); -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; -import "react-table/react-table.css"; -import { DateField } from "../../../../fields/DateField"; -import { AclPrivate, AclReadonly, DataSym, Doc, DocListCast, Field, Opt } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { listSpec } from "../../../../fields/Schema"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; -import { ImageField } from "../../../../fields/URLField"; -import { GetEffectiveAcl } from "../../../../fields/util"; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../../Utils"; -import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { CompileScript, Transformer, ts } from "../../../util/Scripting"; -import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../globalCssVariables.scss'; -import { ContextMenu } from "../../ContextMenu"; -import '../../../views/DocumentDecorations.scss'; -import { DocumentView } from "../../nodes/DocumentView"; -import { DefaultStyleProvider } from "../../StyleProvider"; -import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; -import { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; -import { MovableColumn } from "./CollectionSchemaMovableColumn"; -import { MovableRow } from "./CollectionSchemaMovableRow"; -import "./CollectionSchemaView.scss"; -import { CollectionView } from "../CollectionView"; - - -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] -]); - -export interface SchemaTableProps { - Document: Doc; // child doc - dataDoc?: Doc; - PanelHeight: () => number; - PanelWidth: () => number; - childDocs?: Doc[]; - CollectionView: Opt; - ContainingCollectionView: Opt; - ContainingCollectionDoc: Opt; - fieldKey: string; - renderDepth: number; - deleteDocument?: (document: Doc | Doc[]) => boolean; - addDocument?: (document: Doc | Doc[]) => boolean; - moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; - ScreenToLocalTransform: () => Transform; - active: (outsideReaction: boolean | undefined) => boolean; - onDrop: (e: React.DragEvent, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; - isSelected: (outsideReaction?: boolean) => boolean; - isFocused: (document: Doc, outsideReaction: boolean) => boolean; - setFocused: (document: Doc) => void; - setPreviewDoc: (document: Opt) => void; - columns: SchemaHeaderField[]; - documentKeys: any[]; - headerIsEditing: boolean; - openHeader: (column: any, screenx: number, screeny: number) => void; - onClick: (e: React.MouseEvent) => void; - onPointerDown: (e: React.PointerEvent) => void; - onResizedChange: (newResized: Resize[], event: any) => void; - setColumns: (columns: SchemaHeaderField[]) => void; - reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => void; - changeColumns: (oldKey: string, newKey: string, addNew: boolean) => void; - setHeaderIsEditing: (isEditing: boolean) => void; - changeColumnSort: (columnField: SchemaHeaderField, descending: boolean | undefined) => void; -} - -@observer -export class SchemaTable extends React.Component { - @observable _cellIsEditing: boolean = false; - @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; - @observable _openCollections: Set = new Set; - - @observable _showDoc: Doc | undefined; - @observable _showDataDoc: any = ""; - @observable _showDocPos: number[] = []; - - @observable _showTitleDropdown: 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 childDocs() { - if (this.props.childDocs) return this.props.childDocs; - - const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - return DocListCast(doc[this.props.fieldKey]); - } - set childDocs(docs: Doc[]) { - const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - doc[this.props.fieldKey] = new List(docs); - } - - @computed get textWrappedRows() { - return Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); - } - set textWrappedRows(textWrappedRows: string[]) { - this.props.Document.textwrappedSchemaRows = new List(textWrappedRows); - } - - @computed get resized(): { id: string, value: number }[] { - return this.props.columns.reduce((resized, shf) => { - (shf.width > -1) && resized.push({ id: shf.heading, value: shf.width }); - return resized; - }, [] as { id: string, value: number }[]); - } - @computed get sorted(): SortingRule[] { - return this.props.columns.reduce((sorted, shf) => { - shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc }); - return sorted; - }, [] as SortingRule[]); - } - - @action - changeSorting = (col: any) => { - this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true); - } - - @action - changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown - - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableColumns(): Column[] { - const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); - const columns: Column[] = []; - const tableIsFocused = this.props.isFocused(this.props.Document, false); - const focusedRow = this._focusedCell.row; - const focusedCol = this._focusedCell.col; - const isEditable = !this.props.headerIsEditing; - - columns.push({ - expander: true, Header: "", width: 58, - Expander: (rowInfo) => { - return rowInfo.original.type !== DocumentType.COL ? (null) : -
(this._openCollections[rowInfo.isExpanded ? "delete" : "add"])(rowInfo.viewIndex))}> - -
; - } - }); - columns.push(...this.props.columns.map(col => { - const icon: IconProp = this.getColumnType(col) === ColumnType.Number ? "hashtag" : this.getColumnType(col) === ColumnType.String ? "font" : - this.getColumnType(col) === ColumnType.Boolean ? "check-square" : this.getColumnType(col) === ColumnType.Doc ? "file" : - this.getColumnType(col) === ColumnType.Image ? "image" : this.getColumnType(col) === ColumnType.List ? "list-ul" : - this.getColumnType(col) === ColumnType.Date ? "calendar" : "align-justify"; - - const keysDropdown = c.heading)} - canAddNew={true} - addNew={false} - onSelect={this.props.changeColumns} - setIsEditing={this.props.setHeaderIsEditing} - docs={this.props.childDocs} - Document={this.props.Document} - dataDoc={this.props.dataDoc} - fieldKey={this.props.fieldKey} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} - active={this.props.active} - openHeader={this.props.openHeader} - icon={icon} - col={col} - // try commenting this out - width={"100%"} - />; - - const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; - const header =
- {keysDropdown} -
this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> - -
-
; - - return { - Header: , - accessor: (doc: Doc) => doc ? Field.toString(doc[col.heading] as Field) : 0, - id: col.heading, - Cell: (rowProps: CellInfo) => { - const rowIndex = rowProps.index; - const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); - const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - - const props: CellProps = { - row: rowIndex, - col: columnIndex, - rowProps: rowProps, - isFocused: isFocused, - changeFocusedCellByIndex: this.changeFocusedCellByIndex, - CollectionView: this.props.CollectionView, - ContainingCollection: this.props.ContainingCollectionView, - Document: this.props.Document, - fieldKey: this.props.fieldKey, - renderDepth: this.props.renderDepth, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - moveDocument: this.props.moveDocument, - setIsEditing: this.setCellIsEditing, - isEditable: isEditable, - setPreviewDoc: this.props.setPreviewDoc, - setComputed: this.setComputed, - getField: this.getField, - showDoc: this.showDoc, - }; - - - switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) { - case ColumnType.Number: return ; - case ColumnType.String: return ; - case ColumnType.Boolean: return ; - case ColumnType.Doc: return ; - case ColumnType.Image: return ; - case ColumnType.List: return ; - case ColumnType.Date: return ; - default: - return ; - } - }, - minWidth: 200, - }; - })); - columns.push({ - Header: , - accessor: (doc: Doc) => 0, - id: "add", - Cell: (rowProps: CellInfo) => { - const rowIndex = rowProps.index; - const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); - const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - return ; - }, - width: 28, - resizable: false - }); - return columns; - } - - - constructor(props: SchemaTableProps) { - super(props); - if (this.props.Document._schemaHeaders === undefined) { - this.props.Document._schemaHeaders = new List([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb", ColumnType.Date), - new SchemaHeaderField("text", "#f1efeb", ColumnType.String), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb", ColumnType.Doc)]); - } - } - - componentDidMount() { - document.addEventListener("keydown", this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown); - } - - tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { - const tableDoc = this.props.Document[DataSym]; - const effectiveAcl = GetEffectiveAcl(tableDoc); - - if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) { - doc.context = this.props.Document; - tableDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); - } - return false; - } - - private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { - return !rowInfo ? {} : { - ScreenToLocalTransform: this.props.ScreenToLocalTransform, - addDoc: this.tableAddDoc, - removeDoc: this.props.deleteDocument, - rowInfo, - rowFocused: !this.props.headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document, true), - textWrapRow: this.toggleTextWrapRow, - rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1, - dropAction: StrCast(this.props.Document.childDropAction), - addDocTab: this.props.addDocTab - }; - } - - private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => { - if (!rowInfo || column) return {}; - - const row = rowInfo.index; - //@ts-ignore - const col = this.columns.map(c => c.heading).indexOf(column!.id); - const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true); - // TODO: editing border doesn't work :( - return { - style: { border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" } - }; - } - - @action setCellIsEditing = (isEditing: boolean) => this._cellIsEditing = isEditing; - - @action - onKeyDown = (e: KeyboardEvent): void => { - if (!this._cellIsEditing && !this.props.headerIsEditing && this.props.isFocused(this.props.Document, true)) {// && this.props.isSelected(true)) { - const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; - this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); - - if (direction) { - const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); - pdoc && this.props.setPreviewDoc(pdoc); - e.stopPropagation(); - } - } else if (e.keyCode === 27) { - this.props.setPreviewDoc(undefined); - e.stopPropagation(); // stopPropagation for left/right arrows - } - } - - changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => { - switch (direction) { - case "tab": return { row: (curRow + 1 === this.childDocs.length ? 0 : curRow + 1), col: curCol + 1 === this.props.columns.length ? 0 : curCol + 1 }; - case "right": return { row: curRow, col: curCol + 1 === this.props.columns.length ? curCol : curCol + 1 }; - case "left": return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 }; - case "up": return { row: curRow === 0 ? curRow : curRow - 1, col: curCol }; - case "down": return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol }; - } - return this._focusedCell; - } - - @action - changeFocusedCellByIndex = (row: number, col: number): void => { - if (this._focusedCell.row !== row || this._focusedCell.col !== col) { - this._focusedCell = { row: row, col: col }; - } - this.props.setFocused(this.props.Document); - } - - @undoBatch - createRow = action(() => { - this.props.addDocument?.(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); - this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col }; - }); - - @undoBatch - @action - createColumn = () => { - let index = 0; - let found = this.props.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; - while (found) { - index++; - found = this.props.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; - } - this.props.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb")); - } - - @action - getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => { - if (doc && field && column.type === ColumnType.Any) { - const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)]; - if (val instanceof ImageField) return ColumnType.Image; - if (val instanceof Doc) return ColumnType.Doc; - if (val instanceof DateField) return ColumnType.Date; - if (val instanceof List) return ColumnType.List; - } - if (column.type && column.type !== 0) { - return column.type; - } - if (columnTypes.get(column.heading)) { - return column.type = columnTypes.get(column.heading)!; - } - return column.type = ColumnType.Any; - } - - @undoBatch - @action - toggleTextwrap = async () => { - const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); - if (textwrappedRows.length) { - this.props.Document.textwrappedSchemaRows = new List([]); - } else { - const docs = DocListCast(this.props.Document[this.props.fieldKey]); - const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); - this.props.Document.textwrappedSchemaRows = new List(allRows); - } - } - - @action - toggleTextWrapRow = (doc: Doc): void => { - const textWrapped = this.textWrappedRows; - const index = textWrapped.findIndex(id => doc[Id] === id); - - index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]); - - this.textWrappedRows = textWrapped; - } - - @computed - get reactTable() { - const children = this.childDocs; - const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false); - const expanded: { [name: string]: any } = {}; - Array.from(this._openCollections.keys()).map(col => expanded[col.toString()] = true); - const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( - - return (row.original.type !== DocumentType.COL) ? (null) : -
} - - />; - } - - onContextMenu = (e: React.MouseEvent): void => { - ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); - } - - getField = (row: number, col?: number) => { - const docs = this.childDocs; - - row = row % docs.length; - while (row < 0) row += docs.length; - const columns = this.props.columns; - const doc = docs[row]; - if (col === undefined) { - return doc; - } - if (col >= 0 && col < columns.length) { - const column = this.props.columns[col].heading; - return doc[column]; - } - return undefined; - } - - createTransformer = (row: number, col: number): Transformer => { - const self = this; - const captures: { [name: string]: Field } = {}; - - const transformer: ts.TransformerFactory = context => { - return root => { - function visit(node: ts.Node) { - node = ts.visitEachChild(node, visit, context); - if (ts.isIdentifier(node)) { - const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; - const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; - if (isntPropAccess && isntPropAssign) { - if (node.text === "$r") { - return ts.createNumericLiteral(row.toString()); - } else if (node.text === "$c") { - return ts.createNumericLiteral(col.toString()); - } else if (node.text === "$") { - if (ts.isCallExpression(node.parent)) { - // captures.doc = self.props.Document; - // captures.key = self.props.fieldKey; - } - } - } - } - - return node; - } - return ts.visitNode(root, visit); - }; - }; - - // const getVars = () => { - // return { capturedVariables: captures }; - // }; - - return { transformer, /*getVars*/ }; - } - - setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => { - script = - `const $ = (row:number, col?:number) => { - const rval = (doc as any)[key][row + ${row}]; - return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading]; - } - return ${script}`; - const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); - if (compiled.compiled) { - doc[field] = new ComputedField(compiled); - return true; - } - return false; - } - - @action - showDoc = (doc: Doc | undefined, dataDoc?: Doc, screenX?: number, screenY?: number) => { - this._showDoc = doc; - if (dataDoc && screenX && screenY) { - this._showDocPos = this.props.ScreenToLocalTransform().transformPoint(screenX, screenY); - } - } - - onOpenClick = () => { - this._showDoc && this.props.addDocTab(this._showDoc, "add:right"); - } - - getPreviewTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(- this.borderWidth - 4 - this.tableWidth, - this.borderWidth); - } - - render() { - const preview = ""; - return
this.props.active(true) && e.stopPropagation()} - onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > - {this.reactTable} - {this.props.Document._chromeHidden ? undefined :
+ new
} - {!this._showDoc ? (null) : -
- 150} - PanelHeight={() => 150} - ScreenToLocalTransform={this.getPreviewTransform} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - moveDocument={this.props.moveDocument} - whenChildContentsActiveChanged={emptyFunction} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse}> - -
} -
; - } -} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index ecf4c0901..34488ffbe 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -8,7 +8,7 @@ import { emptyPath, OmitKeys, Without } from "../../../Utils"; import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CollectionSchemaView } from "../collections/schemaView/CollectionSchemaView"; +import { CollectionSchemaView } from "../collections/collectionSchema/CollectionSchemaView"; import { CollectionView } from "../collections/CollectionView"; import { InkingStroke } from "../InkingStroke"; import { PresElementBox } from "../presentationview/PresElementBox"; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index a671c955d..6a2325342 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -18,7 +18,7 @@ import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; import { Transform } from '../../util/Transform'; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionSchemaView, ColumnType } from "../collections/schemaView/CollectionSchemaView"; +import { CollectionSchemaView, ColumnType } from "../collections/collectionSchema/CollectionSchemaView"; import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; @@ -522,7 +522,7 @@ export class SearchBox extends ViewBoxBaseComponent window.location.assign(Utils.prepend("/logout"))}> Logoff -
+
DocServer.UPDATE_SERVER_CACHE()}> {`UI project`} @@ -534,10 +534,10 @@ export class SearchBox extends ViewBoxBaseComponent
CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}> New -
+
CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}> Snapshot -
+
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 74cf934f2..a53fa542e 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -3,7 +3,7 @@ import { serializable, primitive } from "serializr"; import { ObjectField } from "./ObjectField"; import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; -import { ColumnType } from "../client/views/collections/schemaView/CollectionSchemaView"; +import { ColumnType } from "../client/views/collections/collectionSchema/CollectionSchemaView"; export const PastelSchemaPalette = new Map([ // ["pink1", "#FFB4E8"], -- cgit v1.2.3-70-g09d2