import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Popup, PopupTrigger, Type } from 'browndash-components'; import { ObservableMap, action, computed, makeObservable, observable, observe } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from '../../../../fields/Types'; import { DocUtils, Docs, DocumentOptions, FInfo } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, undoable } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider, StyleProp } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export enum ColumnType { Number, String, Boolean, Date, Image, RTF, Enumeration, Any, } export const FInfotoColType: { [key: string]: ColumnType } = { string: ColumnType.String, number: ColumnType.Number, boolean: ColumnType.Boolean, date: ColumnType.Date, image: ColumnType.Image, rtf: ColumnType.RTF, enumeration: ColumnType.Enumeration, }; const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text']; @observer export class CollectionSchemaView extends CollectionSubView() { private _keysDisposer: any; private _previewRef: HTMLDivElement | null = null; private _makeNewColumn: boolean = false; private _documentOptions: DocumentOptions = new DocumentOptions(); private _tableContentRef: HTMLDivElement | null = null; private _menuTarget = React.createRef(); constructor(props: any) { super(props); makeObservable(this); } static _rowHeight: number = 50; static _rowSingleLineHeight: number = 32; public static _minColWidth: number = 25; public static _rowMenuWidth: number = 60; public static _previewDividerWidth: number = 4; public static _newNodeInputHeight: number = Number(SCHEMA_NEW_NODE_HEIGHT); public fieldInfos = new ObservableMap(); @observable _menuKeys: string[] = []; @observable _rowEles: ObservableMap = new ObservableMap(); @observable _colEles: HTMLDivElement[] = []; @observable _displayColumnWidths: number[] | undefined = undefined; @observable _columnMenuIndex: number | undefined = undefined; @observable _newFieldWarning: string = ''; @observable _makeNewField: boolean = false; @observable _newFieldDefault: any = 0; @observable _newFieldType: ColumnType = ColumnType.Number; @observable _menuValue: string = ''; @observable _filterColumnIndex: number | undefined = undefined; @observable _filterSearchValue: string = ''; @observable _selectedCol: number = 0; @observable _selectedCells: Array = []; @observable _mouseCoordinates = { x: 0, y: 0 }; @observable _lowestSelectedIndex = -1; //lowest index among selected rows; used to properly sync dragged docs with cursor position @observable _relCursorIndex = -1; //cursor index relative to the current selected cells @observable _draggedColIndex = 0; @observable _colBeingDragged = false; // target HTMLelement portal for showing a popup menu to edit cell values. public get MenuTarget() { return this._menuTarget.current; } @computed get _selectedDocs() { // get all selected documents then filter out any whose parent is not this schema document const selected = SelectionManager.Docs.filter(doc => this.childDocs.includes(doc)); // SelectionManager... filter(doc => this.childDocs.includes(doc)) //DocCast(doc.embedContainer)[DocData] === this.dataDoc //SelectionManager.Docs.forEach(doc => console.log("index: " + this.rowIndex(doc) + " equal: " + Doc.AreProtosEqual(DocCast(doc.embedContainer), this.Document))); if (!selected.length) { for (const sel of SelectionManager.Docs) { const contextPath = DocumentManager.GetContextPath(sel, true); if (contextPath.includes(this.Document)) { const parentInd = contextPath.indexOf(this.Document); return parentInd < contextPath.length - 1 ? [contextPath[parentInd + 1]] : []; } } } return selected; } @computed get documentKeys() { return Array.from(this.fieldInfos.keys()); } @computed get previewWidth() { return NumCast(this.layoutDoc.schema_previewWidth); } @computed get tableWidth() { return this._props.PanelWidth() - this.previewWidth - (this.previewWidth === 0 ? 0 : CollectionSchemaView._previewDividerWidth); } @computed get columnKeys() { return Cast(this.layoutDoc.schema_columnKeys, listSpec('string'), defaultColumnKeys); } @computed get rowKeys() { return Cast(this.layoutDoc.schema_rowKeys, listSpec('string'), []); } @computed get storedColumnWidths() { const widths = NumListCast( this.layoutDoc.schema_columnWidths, this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length) ); const totalWidth = widths.reduce((sum, width) => sum + width, 0); //If the total width of all columns is not the width of the schema table minus the width of the row menu, resize them appropriately if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) { return widths.map(w => (w / totalWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)); } return widths; } @computed get rowHeights() { const heights = this.childDocs.map(() => this.rowHeightFunc()); return heights; } @computed get displayColumnWidths() { return this._displayColumnWidths ?? this.storedColumnWidths; } @computed get sortField() { return StrCast(this.layoutDoc.sortField); } @computed get sortDesc() { return BoolCast(this.layoutDoc.sortDesc); } componentDidMount() { this._props.setContentViewBox?.(this); document.addEventListener('keydown', this.onKeyDown); Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1])); this._keysDisposer = observe( this.dataDoc[this.fieldKey ?? 'data'] as List, change => { switch (change.type as any) { case 'splice': // prettier-ignore (change as any).added.forEach((doc: Doc) => // for each document added Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself) Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author')))))); break; case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list } }, true ); } componentWillUnmount() { this._keysDisposer?.(); document.removeEventListener('keydown', this.onKeyDown); } rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc); @action onKeyDown = (e: KeyboardEvent) => { if (this._selectedDocs.length > 0) { switch (e.key) { case 'ArrowDown': { const lastDoc = this._selectedDocs.lastElement(); const lastIndex = this.rowIndex(lastDoc); const curDoc = this.sortedDocs.docs[lastIndex]; if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) { const newDoc = this.sortedDocs.docs[lastIndex + 1]; if (this._selectedDocs.includes(newDoc)) { SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc)); this.deselectCell(curDoc); } else { const shift: boolean = e.shiftKey; const ctrl: boolean = e.ctrlKey; this.selectCell(newDoc, this._selectedCol, shift, ctrl); this.scrollToDoc(newDoc, {}); } } e.stopPropagation(); e.preventDefault(); } break; case 'ArrowUp': { const firstDoc = this._selectedDocs.lastElement(); const firstIndex = this.rowIndex(firstDoc); const curDoc = this.sortedDocs.docs[firstIndex]; if (firstIndex > 0 && firstIndex < this.childDocs.length) { const newDoc = this.sortedDocs.docs[firstIndex - 1]; if (this._selectedDocs.includes(newDoc)) { SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc)); this.deselectCell(curDoc); } else { const shift: boolean = e.shiftKey; const ctrl: boolean = e.ctrlKey; this.selectCell(newDoc, this._selectedCol, shift, ctrl); this.scrollToDoc(newDoc, {}); } } e.stopPropagation(); e.preventDefault(); } break; case 'ArrowRight': if (this._selectedCells) { this._selectedCol = Math.min(this._colEles.length - 1, this._selectedCol + 1); } else if (this._selectedDocs.length > 0) { this.selectCell(this._selectedDocs[0], 0, false, false); } break; case 'ArrowLeft': if (this._selectedCells) { this._selectedCol = Math.max(0, this._selectedCol - 1); } else if (this._selectedDocs.length > 0) { this.selectCell(this._selectedDocs[0], 0, false, false); } break; case 'Backspace': { undoable(() => this.removeDocument(this._selectedDocs), 'delete schema row'); break; } case 'Escape': { this.deselectAllCells(); } } } }; @action changeSelectedCellColumn = () => {}; @undoBatch setColumnSort = (field: string | undefined, desc: boolean = false) => { this.layoutDoc.sortField = field; this.layoutDoc.sortDesc = desc; }; addRow = (doc: Doc | Doc[]) => this.addDocument(doc); @undoBatch changeColumnKey = (index: number, newKey: string, defaultVal?: any) => { if (!this.documentKeys.includes(newKey)) { this.addNewKey(newKey, defaultVal); } let currKeys = [...this.columnKeys]; currKeys[index] = newKey; this.layoutDoc.schema_columnKeys = new List(currKeys); }; @undoBatch addColumn = (key: string, defaultVal?: any) => { if (!this.documentKeys.includes(key)) { this.addNewKey(key, defaultVal); } const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1); const currWidths = this.storedColumnWidths.slice(); currWidths.splice(0, 0, newColWidth); const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); this.layoutDoc.schema_columnWidths = new List(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); let currKeys = this.columnKeys.slice(); currKeys.splice(0, 0, key); this.layoutDoc.schema_columnKeys = new List(currKeys); }; @action addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[DocData][key] = defaultVal)); @undoBatch removeColumn = (index: number) => { if (this.columnKeys.length === 1) return; const currWidths = this.storedColumnWidths.slice(); currWidths.splice(index, 1); const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); this.layoutDoc.schema_columnWidths = new List(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); let currKeys = this.columnKeys.slice(); currKeys.splice(index, 1); this.layoutDoc.schema_columnKeys = new List(currKeys); }; @action startResize = (e: any, index: number) => { this._displayColumnWidths = this.storedColumnWidths; setupMoveUpEvents(this, e, (e, delta) => this.resizeColumn(e, index), this.finishResize, emptyFunction); }; @action resizeColumn = (e: PointerEvent, index: number) => { if (this._displayColumnWidths) { let shrinking; let growing; let change = e.movementX; if (index !== 0) { growing = change < 0 ? index : index - 1; shrinking = change < 0 ? index - 1 : index; } if (shrinking === undefined || growing === undefined) return true; change = Math.abs(change); if (this._displayColumnWidths[shrinking] - change < CollectionSchemaView._minColWidth) { change = this._displayColumnWidths[shrinking] - CollectionSchemaView._minColWidth; } this._displayColumnWidths[shrinking] -= change * this.ScreenToLocalBoxXf().Scale; this._displayColumnWidths[growing] += change * this.ScreenToLocalBoxXf().Scale; return false; } return true; }; @action finishResize = () => { this.layoutDoc.schema_columnWidths = new List(this._displayColumnWidths); this._displayColumnWidths = undefined; }; @undoBatch moveColumn = (fromIndex: number, toIndex: number) => { if (this._selectedCol === fromIndex) this._selectedCol = toIndex; else if (toIndex === this._selectedCol) this._selectedCol = fromIndex; //keeps selected cell consistent let currKeys = this.columnKeys.slice(); currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnKeys = new List(currKeys); let currWidths = this.storedColumnWidths.slice(); currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnWidths = new List(currWidths); this._draggedColIndex = toIndex; }; @action dragColumn = (e: PointerEvent, index: number) => { this._draggedColIndex = index; this._colBeingDragged = true; const dragData = new DragManager.ColumnDragData(index); const dragEles = [this._colEles[index]]; this.childDocs.forEach(doc => dragEles.push(this._rowEles.get(doc).children[1].children[index])); DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y); // document.removeEventListener('pointermove', this.highlightDropColumn); // document.addEventListener('pointermove', this.highlightDropColumn); // let stopHighlight = (e: PointerEvent) => { // document.removeEventListener('pointermove', this.highlightDropColumn); // document.removeEventListener('pointerup', stopHighlight); // }; // document.addEventListener('pointerup', stopHighlight); return true; }; findColDropIndex = (mouseX: number) => { let index: number | undefined; this.displayColumnWidths.reduce((total, curr, i) => { if (total <= mouseX && total + curr >= mouseX) { if (mouseX <= total + curr) index = i; else index = i + 1; } return total + curr; }, 2 * CollectionSchemaView._rowMenuWidth); //probably prone to issues; find better implementation (!!!) return index; }; /** * Calculates the relative index of the cursor in the group of selected rows, ie. * if five rows are selected and the cursor is in the middle row, its relative index would be 2. * Used to align actively dragged documents properly with the cursor. * @param mouseY the initial Y position of the cursor on drag */ @action setRelCursorIndex = (mouseY: number) => { this._mouseCoordinates.y = mouseY; //updates this.rowDropIndex computed value to overwrite the old cached value let rowHeight = CollectionSchemaView._rowHeight; let adjInitMouseY = mouseY - rowHeight - 100; //rowHeight: height of the column menu cells | 100: height of the top menu let yOffset = this._lowestSelectedIndex * rowHeight; const heights = this._selectedDocs.map(() => this.rowHeightFunc()); let index: number = 0; heights.reduce((total, curr, i) => { if (total <= adjInitMouseY && total + curr >= adjInitMouseY) { if (adjInitMouseY <= total + curr) index = i; else index = i + 1; } return total + curr; }, yOffset); this._relCursorIndex = index; }; //Uses current mouse position to calculate the indexes of actively dragged docs findRowDropIndex = (mouseY: number) => { let rowHeight = CollectionSchemaView._rowHeight; let index: number = 0; this.rowHeights.reduce((total, curr, i) => { if (total <= mouseY && total + curr >= mouseY) { if (mouseY <= total + curr) index = i; else index = i + 1; } return total + curr; }, rowHeight); //fix index if selected rows are dragged out of bounds let adjIndex = index - this._relCursorIndex; let maxY = this.rowHeights.reduce((total, curr) => total + curr, 0) + rowHeight; if (mouseY > maxY) adjIndex = this.childDocs.length - 1; else if (adjIndex <= 0) adjIndex = 0; return adjIndex; }; @action highlightDraggedColumn = (index: number) => { this._colEles.forEach((colRef, i) => { let edgeStyle = ''; if (i === index) edgeStyle = `solid 2px ${Colors.MEDIUM_BLUE}`; //border styles of menu cell colRef.style.borderLeft = edgeStyle; colRef.style.borderRight = edgeStyle; colRef.style.borderTop = edgeStyle; for (let doc = 0; doc < this.childDocs.length; ++doc) { if (i === this._selectedCol && this._selectedDocs.includes(this.childDocs[doc])) { continue; } else { this._rowEles.get(this.childDocs[doc]).children[1].children[i].style.borderLeft = edgeStyle; this._rowEles.get(this.childDocs[doc]).children[1].children[i].style.borderRight = edgeStyle; if (doc === this.childDocs.length - 1) { this._rowEles.get(this.childDocs[doc]).children[1].children[i].style.borderBottom = edgeStyle; } } } }); }; @action addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref); @action setColRef = (index: number, ref: HTMLDivElement) => { if (this._colEles.length <= index) { this._colEles.push(ref); } else { this._colEles[index] = ref; } }; @action addDocToSelection = (doc: Doc, extendSelection: boolean, index: number) => { const rowDocView = DocumentManager.Instance.getDocumentView(doc); if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection); }; @action clearSelection = () => { SelectionManager.DeselectAll(); this.deselectAllCells(); }; selectRows = (doc: Doc, lastSelected: Doc) => { const index = this.rowIndex(doc); const lastSelectedRow = this.rowIndex(lastSelected); const startRow = Math.min(lastSelectedRow, index); const endRow = Math.max(lastSelectedRow, index); for (let i = startRow; i <= endRow; i++) { const currDoc = this.sortedDocs.docs[i]; if (!this._selectedDocs.includes(currDoc)) { this.selectCell(currDoc, this._selectedCol, false, true); } } }; @action selectCell = (doc: Doc, col: number, shiftKey: boolean, ctrlKey: boolean) => { if (!shiftKey && !ctrlKey) this.clearSelection(); !this._selectedCells && (this._selectedCells = []); !shiftKey && this._selectedCells && this._selectedCells.push(doc); let index = this.rowIndex(doc); if (!this) return; const lastSelected = Array.from(this._selectedDocs).lastElement(); if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRows(doc, lastSelected); else if (ctrlKey) { if (lastSelected && this._selectedDocs.includes(doc)) { SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(doc)); this.deselectCell(doc); } else this.addDocToSelection(doc, true, index); } else this.addDocToSelection(doc, false, index); this._selectedCol = col; if (this._lowestSelectedIndex === -1 || index < this._lowestSelectedIndex) this._lowestSelectedIndex = index; //let selectedIndexes: Array = this._selectedCells.map(doc => this.rowIndex(doc)); }; @action deselectCell = (doc: Doc) => { this._selectedCells && (this._selectedCells = this._selectedCells.filter(d => d !== doc)); if (this.rowIndex(doc) == this._lowestSelectedIndex) this._lowestSelectedIndex = Math.min(...this._selectedDocs.map(doc => this.rowIndex(doc))); }; @action deselectAllCells = () => { this._selectedCells = []; this._lowestSelectedIndex = -1; }; sortedSelectedDocs = () => this.sortedDocs.docs.filter(doc => this._selectedDocs.includes(doc)); @computed get rowDropIndex() { const mouseY = this.ScreenToLocalBoxXf().transformPoint(this._mouseCoordinates.x, this._mouseCoordinates.y)[1]; const index = this.findRowDropIndex(mouseY); return index; } onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.columnDragData) { this._colBeingDragged = false; e.stopPropagation(); this._colEles.forEach((colRef, i) => { //style for menu cell colRef.style.borderLeft = ''; colRef.style.borderRight = ''; colRef.style.borderTop = ''; this.childDocs.forEach(doc => { if (!(this._selectedDocs.includes(doc) && i === this._selectedCol)) { this._rowEles.get(doc).children[1].children[i].style.borderLeft = ''; this._rowEles.get(doc).children[1].children[i].style.borderRight = ''; this._rowEles.get(doc).children[1].children[i].style.borderBottom = ''; } }); }); return true; } const draggedDocs = de.complete.docDragData?.draggedDocuments; if (draggedDocs && super.onInternalDrop(e, de) && !this.sortField) { let map = draggedDocs?.map(doc => this.rowIndex(doc)); console.log(map); this.dataDoc[this.fieldKey ?? 'data'] = new List([...this.sortedDocs.docs]); this.clearSelection(); draggedDocs.forEach(doc => { DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true)); }); this._lowestSelectedIndex = Math.min(...draggedDocs?.map(doc => this.rowIndex(doc))); return true; } return false; }; onExternalDrop = async (e: React.DragEvent): Promise => { super.onExternalDrop(e, {}, undoBatch(action(docus => docus.map((doc: Doc) => this.addDocument(doc))))); }; onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction); @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { const nativeWidth = this._previewRef!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; const movedWidth = this.ScreenToLocalBoxXf().transformDirection(nativeWidth.right - e.clientX, 0)[0]; const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; this.layoutDoc.schema_previewWidth = width; return false; }; menuCallback = (x: number, y: number) => { ContextMenu.Instance.clearItems(); DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true); ContextMenu.Instance.displayMenu(x, y, undefined, true); }; focusDocument = (doc: Doc, options: FocusViewOptions) => { Doc.BrushDoc(doc); this.scrollToDoc(doc, options); return undefined; }; scrollToDoc = (doc: Doc, options: FocusViewOptions) => { const found = this._tableContentRef && Array.from(this._tableContentRef.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); if (found) { const rect = found.getBoundingClientRect(); const localRect = this.ScreenToLocalBoxXf().transformBounds(rect.left, rect.top, rect.width, rect.height); if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this._props.PanelHeight()) { let focusSpeed = options.zoomTime ?? 50; smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc); return focusSpeed; } } }; @computed get fieldDefaultInput() { switch (this._newFieldType) { case ColumnType.Number: return e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; case ColumnType.Boolean: return ( <> e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} /> {this._newFieldDefault ? 'true' : 'false'} ); case ColumnType.String: return e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; } } onSearchKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Enter': this._menuKeys.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuKeys[0]) : action(() => (this._makeNewField = true))(); break; case 'Escape': this.closeColumnMenu(); break; } }; @action setKey = (key: string, defaultVal?: any) => { if (this._makeNewColumn) { this.addColumn(key, defaultVal); } else { this.changeColumnKey(this._columnMenuIndex!, key, defaultVal); } this.closeColumnMenu(); }; setColumnValues = (key: string, value: string) => { const selectedDocs: Doc[] = new Array(); this.childDocs.forEach(doc => { let docIsSelected = this._selectedCells && !(this._selectedCells?.filter(d => d === doc).length === 0); if (docIsSelected) { selectedDocs.push(doc); } }); if (selectedDocs.length === 1) { this.childDocs.forEach(doc => KeyValueBox.SetField(doc, key, value)); } else { selectedDocs.forEach(doc => KeyValueBox.SetField(doc, key, value)); } return true; }; setSelectedColumnValues = (key: string, value: string) => { this.childDocs.forEach(doc => { let docIsSelected = this._selectedCells && !(this._selectedCells?.filter(d => d === doc).length === 0); if (docIsSelected) { KeyValueBox.SetField(doc, key, value); } }); return true; }; @action openColumnMenu = (index: number, newCol: boolean) => { this._makeNewColumn = false; this._columnMenuIndex = index; this._menuValue = ''; this._menuKeys = this.documentKeys; this._makeNewField = false; this._newFieldWarning = ''; this._makeNewField = false; this._makeNewColumn = newCol; }; @action closeColumnMenu = () => (this._columnMenuIndex = undefined); @action openFilterMenu = (index: number) => { this._filterColumnIndex = index; this._filterSearchValue = ''; }; @action closeFilterMenu = () => (this._filterColumnIndex = undefined); openContextMenu = (x: number, y: number, index: number) => { this.closeColumnMenu(); this.closeFilterMenu(); ContextMenu.Instance.clearItems(); ContextMenu.Instance.addItem({ description: 'Change field', event: () => this.openColumnMenu(index, false), icon: 'pencil-alt', }); ContextMenu.Instance.addItem({ description: 'Filter field', event: () => this.openFilterMenu(index), icon: 'filter', }); ContextMenu.Instance.addItem({ description: 'Delete column', event: () => this.removeColumn(index), icon: 'trash', }); ContextMenu.Instance.displayMenu(x, y, undefined, false); }; @action updateKeySearch = (e: React.ChangeEvent) => { this._menuValue = e.target.value; this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); }; getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] == field); removeFieldFilters = (field: string) => { this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(Doc.FilterSep)[1], 'remove')); }; onFilterKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Enter': case 'Escape': this.closeFilterMenu(); break; } }; @action updateFilterSearch = (e: React.ChangeEvent) => (this._filterSearchValue = e.target.value); @computed get newFieldMenu() { return (
{ this._newFieldType = ColumnType.Number; this._newFieldDefault = 0; })} /> number
{ this._newFieldType = ColumnType.Boolean; this._newFieldDefault = false; })} /> boolean
{ this._newFieldType = ColumnType.String; this._newFieldDefault = ''; })} /> string
value: {this.fieldDefaultInput}
{this._newFieldWarning}
{ if (this.documentKeys.includes(this._menuValue)) { this._newFieldWarning = 'Field already exists'; } else if (this._menuValue.length === 0) { this._newFieldWarning = 'Field cannot be an empty string'; } else { this.setKey(this._menuValue, this._newFieldDefault); } this._columnMenuIndex = undefined; })}> done
); } onKeysPassiveWheel = (e: WheelEvent) => { // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); e.stopPropagation(); }; _oldKeysWheel: any; @computed get keysDropdown() { return (
{ e.stopPropagation(); this._makeNewField = true; })}> + new field
{ this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel); this._oldKeysWheel = r; r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false }); }}> {this._menuKeys.map(key => (
{ e.stopPropagation(); this.setKey(key); }}>

{key} {this.fieldInfos.get(key)!.fieldType ? ':' : ''} {this.fieldInfos.get(key)!.fieldType}   {this.fieldInfos.get(key)!.description}

))}
); } @computed get renderColumnMenu() { const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); return (
e.stopPropagation()} /> {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
); } get renderKeysMenu() { //console.log('RNDERMENUT:' + this._columnMenuIndex); return (
e.stopPropagation()} /> {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
); } @computed get renderFilterOptions() { const keyOptions: string[] = []; const columnKey = this.columnKeys[this._filterColumnIndex!]; const allDocs = DocListCast(this.dataDoc[this._props.fieldKey]); allDocs.forEach(doc => { const value = StrCast(doc[columnKey]); if (!keyOptions.includes(value) && value !== '' && (this._filterSearchValue === '' || value.includes(this._filterSearchValue))) { keyOptions.push(value); } }); const filters = StrListCast(this.Document._childFilters); return keyOptions.map(key => { let bool = false; if (filters !== undefined) { const ind = filters.findIndex(filter => filter.split(Doc.FilterSep)[1] === key); const fields = ind === -1 ? undefined : filters[ind].split(Doc.FilterSep); bool = fields ? fields[2] === 'check' : false; } return (
e.stopPropagation()} onClick={e => e.stopPropagation()} onChange={action(e => { if (e.target.checked) { Doc.setDocFilter(this.Document, columnKey, key, 'check'); } else { Doc.setDocFilter(this.Document, columnKey, key, 'remove'); } })} checked={bool} /> {key}
); }); } @computed get renderFilterMenu() { const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); return (
e.stopPropagation()} /> {this.renderFilterOptions}
{ e.stopPropagation(); this.closeFilterMenu(); })}> done
); } @action onPointerMove = (e: React.PointerEvent) => { if (DragManager.docsBeingDragged.length) { this._mouseCoordinates = { x: e.clientX, y: e.clientY }; } if (this._colBeingDragged) { let newIndex = this.findColDropIndex(e.clientX); if (newIndex != this._draggedColIndex) this.moveColumn(this._draggedColIndex, newIndex ?? this._draggedColIndex); this._draggedColIndex = newIndex ? newIndex : this._draggedColIndex; this.highlightDraggedColumn(newIndex ?? this._draggedColIndex); } }; @computed get sortedDocs() { const field = StrCast(this.layoutDoc.sortField); const desc = BoolCast(this.layoutDoc.sortDesc); // is this an ascending or descending sort const staticDocs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d)); const docs = !field ? staticDocs : [...staticDocs].sort((docA, docB) => { // this sorts the documents based on the selected field. returning -1 for a before b, 0 for a = b, 1 for a > b const aStr = Field.toString(docA[field] as Field); const bStr = Field.toString(docB[field] as Field); var out = 0; if (aStr < bStr) out = -1; if (aStr > bStr) out = 1; if (desc) out *= -1; return out; }); docs.splice(this.rowDropIndex, 0, ...DragManager.docsBeingDragged); return { docs }; } rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight); sortedDocsFunc = () => this.sortedDocs; isContentActive = () => this._props.isSelected() || this._props.isContentActive(); screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0); previewWidthFunc = () => this.previewWidth; onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); _oldWheel: any; render() { return (
this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)} onPointerMove={e => this.onPointerMove(e)}>
this._props.isContentActive() && e.stopPropagation()} ref={ele => { // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); (this._oldWheel = ele)?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }}>
this.openColumnMenu(-1, true)} icon="plus" />} trigger={PopupTrigger.CLICK} type={Type.TERT} isOpen={this._columnMenuIndex !== -1 ? false : undefined} popup={this.renderKeysMenu} />
{this.columnKeys.map((key, index) => ( ))}
{this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} (this._tableContentRef = ref)} /> {this.layoutDoc.chromeHidden ? null : (
(value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')} placeholder={"Type text to create note or ':' to create specific type"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} />
)}
{this.previewWidth > 0 &&
} {this.previewWidth > 0 && (
(this._previewRef = ref)}> {Array.from(this._selectedDocs).lastElement() && ( )}
)}
); } } interface CollectionSchemaViewDocsProps { schema: CollectionSchemaView; setRef: (ref: HTMLDivElement | null) => void; childDocs: () => { docs: Doc[] }; rowHeight: () => number; } @observer class CollectionSchemaViewDocs extends React.Component { render() { return (
{this.props.childDocs().docs.map((doc: Doc, index: number) => (
))}
); } } interface CollectionSchemaViewDocProps { schema: CollectionSchemaView; index: number; doc: Doc; rowHeight: () => number; } @observer class CollectionSchemaViewDoc extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } tableWidthFunc = () => this._props.schema.tableWidth; screenToLocalXf = () => this._props.schema.ScreenToLocalBoxXf().translate(0, -this._props.rowHeight() - this._props.index * this._props.rowHeight()); noOpacityStyleProvider = (doc: Opt, props: Opt, property: string) => { if (property === StyleProp.Opacity) return 1; return DefaultStyleProvider(doc, props, property); }; isRowContentActive = () => this._props.schema.isContentActive() || this._props.schema._props.isSelected() || this._props.schema._props.isAnyChildContentActive(); render() { return ( ); } }