import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction, numberRange } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; import { StrCast } from '../../../fields/Types'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { CompileScript } from '../../util/Scripting'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, undoable } from '../../util/UndoManager'; import { EditableView } from '../EditableView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DocumentView } from '../nodes/DocumentView'; import { ImportElementBox } from '../nodes/importBox/ImportElementBox'; import { CollectionStackingView } from './CollectionStackingView'; import './CollectionStackingView.scss'; interface CMVFieldRowProps { rows: () => number; headings: () => object[]; Doc: Doc; chromeHidden?: boolean; heading: string; headingObject: SchemaHeaderField | undefined; docList: Doc[]; parent: CollectionStackingView; panelWidth: () => number; columnWidth: () => number; pivotField: string; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; setDocHeight: (key: string, thisHeight: number) => void; sectionRefs: Element[]; showHandle: boolean; } @observer export class CollectionMasonryViewFieldRow extends ObservableReactComponent { constructor(props: CMVFieldRowProps) { super(props); makeObservable(this); } @observable private _background = 'inherit'; @observable private _createEmbeddingSelected: boolean = false; @observable private heading: string = ''; @observable private color: string = '#f1efeb'; @observable private collapsed: boolean = false; @observable private _paletteOn = false; private set _heading(value: string) { runInAction(() => { this._props.headingObject && (this._props.headingObject.heading = this.heading = value); }); } private set _color(value: string) { runInAction(() => { this._props.headingObject && (this._props.headingObject.color = this.color = value); }); } private set _collapsed(value: boolean) { runInAction(() => { this._props.headingObject && (this._props.headingObject.collapsed = this.collapsed = value); }); } private _dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject = React.createRef(); private _contRef: React.RefObject = React.createRef(); private _ele: HTMLDivElement | null = null; createRowDropRef = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Doc); else if (this._ele) this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1); this._ele = ele; }; @action componentDidMount() { this.heading = this._props.headingObject?.heading || ''; this.color = this._props.headingObject?.color || '#f1efeb'; this.collapsed = this._props.headingObject?.collapsed || false; this._ele && this.props.sectionRefs.push(this._ele); } componentWillUnmount() { this._ele && this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1); this._ele = null; } getTrueHeight = () => { if (this.collapsed) { this._props.setDocHeight(this.heading, 20); } else { const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; // +15 accounts for the group header const transformScale = this._props.screenToLocalTransform().Scale; const trueHeight = rawHeight * transformScale; this._props.setDocHeight(this.heading, trueHeight); } }; @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { this._createEmbeddingSelected = false; if (de.complete.docDragData) { const key = this._props.pivotField; const castedValue = this.getValue(this.heading); if (this._props.parent.onInternalDrop(e, de)) { key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); } return true; } return false; }); getValue = (value: string) => { const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; if (value.toLowerCase().indexOf('true') > -1) return true; if (value.toLowerCase().indexOf('false') > -1) return false; return value; }; @action headingChanged = (value: string /* , shiftDown?: boolean */) => { this._createEmbeddingSelected = false; const key = this._props.pivotField; const castedValue = this.getValue(value); if (castedValue) { if (this._props.parent.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) || 0 > -1) { return false; } key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); this._heading = castedValue.toString(); return true; } return false; }; @action changeColumnColor = (color: string) => { this._createEmbeddingSelected = false; this._color = color; }; pointerEnteredRow = action(() => { SnappingManager.IsDragging && (this._background = '#b4b4b4'); }); @action pointerLeaveRow = () => { this._createEmbeddingSelected = false; this._background = 'inherit'; }; @action addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; this._createEmbeddingSelected = false; const { pivotField } = this._props; const newDoc = Docs.Create.TextDocument(value, { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value }); DocumentView.SetSelectOnLoad(newDoc); pivotField && (newDoc['$' + pivotField] = this.getValue(this._props.heading)); const docs = this._props.parent.childDocList; return docs ? !!docs.splice(0, 0, newDoc) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list) }; deleteRow = undoable( action(() => { this._createEmbeddingSelected = false; const key = this._props.pivotField; key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true)); if (this._props.parent.colHeaderData && this._props.headingObject) { const index = this._props.parent.colHeaderData.indexOf(this._props.headingObject); this._props.parent.colHeaderData.splice(index, 1); } }), 'delete row' ); @action collapseSection = (e: PointerEvent) => { this._createEmbeddingSelected = false; this.toggleVisibility(); e.stopPropagation(); }; headerMove = (e: PointerEvent) => { const embedding = Doc.MakeEmbedding(this._props.Doc); const key = this._props.pivotField; let value = this.getValue(this.heading); value = typeof value === 'string' ? `"${value}"` : value; const script = `return doc.${key} === ${value}`; const compiled = CompileScript(script, { params: { doc: Doc.name } }); if (compiled.compiled) { embedding.viewSpecScript = new ScriptField(compiled); DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY); } return true; }; @action headerDown = (e: React.PointerEvent) => { if (e.button === 0 && !e.ctrlKey) { setupMoveUpEvents(this, e, this.headerMove, emptyFunction, clickEv => !this._props.chromeHidden && this.collapseSection(clickEv)); this._createEmbeddingSelected = false; } }; renderColorPicker = () => { const selected = this.color; const pink = PastelSchemaPalette.get('pink2'); const purple = PastelSchemaPalette.get('purple4'); const blue = PastelSchemaPalette.get('bluegreen1'); const yellow = PastelSchemaPalette.get('yellow4'); const red = PastelSchemaPalette.get('red2'); const green = PastelSchemaPalette.get('bluegreen7'); const cyan = PastelSchemaPalette.get('bluegreen5'); const orange = PastelSchemaPalette.get('orange1'); const gray = '#f1efeb'; return (
this.changeColumnColor(pink!)} />
this.changeColumnColor(purple!)} />
this.changeColumnColor(blue!)} />
this.changeColumnColor(yellow!)} />
this.changeColumnColor(red!)} />
this.changeColumnColor(gray)} />
this.changeColumnColor(green!)} />
this.changeColumnColor(cyan!)} />
this.changeColumnColor(orange!)} />
); }; toggleEmbedding = action(() => { this._createEmbeddingSelected = true; }); toggleVisibility = () => { this._collapsed = !this.collapsed; }; @action textCallback = (/* char: string */) => this.addDocument('', false); @computed get contentLayout() { const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth()))); return this.collapsed ? null : (
{!this._props.chromeHidden && !StrCast(this._props.Doc.childLayoutString).includes(ImportElementBox.name) ? (
) : null}
list + ` ${this._props.columnWidth()}px`, ''), }}> {this._props.parent.children(this._props.docList)}
); } @computed get headingView() { const noChrome = this._props.chromeHidden; const key = this._props.pivotField; const evContents = this.heading ? this.heading : this._props.type && this._props.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`; const editableHeaderView = evContents} SetValue={this.headingChanged} contents={evContents} oneLine />; return this._props.Doc.miniHeaders ? (
{editableHeaderView}
) : !this._props.headingObject ? null : (
{noChrome ? evContents :
{editableHeaderView}
} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
{this._paletteOn ? this.renderColorPicker() : null}
)} {noChrome ? null : ( )} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
e.stopPropagation()}>
)}
); } render() { const background = this._background; return (
{this.headingView} {this.contentLayout}
); } }