import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { IconButton, Size } from '@dash/components'; import { IReactionDisposer, Lambda, ObservableMap, action, computed, makeObservable, observable, observe, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { ColumnType } from '../../../../fields/SchemaHeaderField'; import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; import { DocUtils } from '../../../documents/DocUtils'; import { Docs, DocumentOptions, FInfo, FInfoFieldType } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, undoable } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { StyleProp } from '../../StyleProp'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaCellField } from './SchemaCellField'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; /** * The schema view offers a spreadsheet-like interface for users to interact with documents. Within the schema, * each doc is represented by its own row. Each column represents a field, for example the author or title fields. * Users can apply varoius filters and sorts to columns to change what is displayed. The schemaview supports equations for * cell linking. * * This class supports the main functionality for choosing which docs to render in the view, applying visual * updates to rows and columns (such as user dragging or sort-related highlighting), applying edits to multiple cells * at once, and applying filters and sorts to columns. It contains SchemaRowBoxes (which themselves contain SchemaTableCells, * and SchemaCellFields) and SchemaColumnHeaders. */ // eslint-disable-next-line @typescript-eslint/no-require-imports const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export const FInfotoColType: { [key in FInfoFieldType]: ColumnType } = { string: ColumnType.String, number: ColumnType.Number, boolean: ColumnType.Boolean, date: ColumnType.Date, richtext: ColumnType.RTF, enum: ColumnType.Enumeration, Doc: ColumnType.Any, list: ColumnType.Any, map: ColumnType.Any, }; const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text']; @observer export class CollectionSchemaView extends CollectionSubView() { private _keysDisposer?: Lambda; private _disposers: { [name: string]: IReactionDisposer } = {}; private _previewRef: HTMLDivElement | null = null; private _makeNewColumn: boolean = false; private _documentOptions: DocumentOptions = new DocumentOptions(); private _tableContentRef: HTMLDivElement | null = null; private _menuTarget = React.createRef(); private _headerRefs: SchemaColumnHeader[] = []; private _eqHighlightColors: Array<[{ r: number; g: number; b: number }, { r: number; g: number; b: number }]> = []; private _oldWheel: HTMLDivElement | null = null; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); const lightenedColor = (r: number, g: number, b:number) => { const lightened = ClientUtils.lightenRGB(r, g, b, 165); return {r: lightened[0], g: lightened[1], b: lightened[2]}} // prettier-ignore const colors = (r: number, g: number, b: number):[{r:number,g:number,b:number},{r:number,g:number,b:number}] => ([{r, g, b}, lightenedColor(r, g, b)]); // prettier-ignore this._eqHighlightColors.push(colors(70, 150, 50)); this._eqHighlightColors.push(colors(180, 70, 20)); this._eqHighlightColors.push(colors(70, 50, 150)); this._eqHighlightColors.push(colors(0, 140, 140)); this._eqHighlightColors.push(colors(140, 30, 110)); this._eqHighlightColors.push(colors(20, 50, 200)); this._eqHighlightColors.push(colors(210, 30, 40)); this._eqHighlightColors.push(colors(120, 130, 30)); this._eqHighlightColors.push(colors(50, 150, 70)); this._eqHighlightColors.push(colors(10, 90, 180)); } 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: boolean | number | string | undefined = 0; @observable _newFieldType: ColumnType = ColumnType.Number; @observable _menuValue: string = ''; @observable _filterColumnIndex: number | undefined = undefined; @observable _filterSearchValue: string = ''; //the current text inside the filter search bar, used to determine which values to display @observable _selectedCol: number = 0; @observable _selectedCells: Array = []; @observable _mouseCoordinates = { x: 0, y: 0, prevX: 0, prevY: 0 }; @observable _lowestSelectedIndex: number = -1; //lowest index among selected rows; used to properly sync dragged docs with cursor position @observable _relCursorIndex: number = -1; //cursor index relative to the current selected cells @observable _draggedColIndex: number = 0; @observable _colBeingDragged: boolean = false; //whether a column is being dragged by the user @observable _colKeysFiltered: boolean = false; @observable _cellTags: ObservableMap = new ObservableMap>(); @observable _highlightedCellsInfo: Array<[doc: Doc, field: string]> = []; @observable _cellHighlightColors: ObservableMap = new ObservableMap(); @observable _containedDocs: Doc[] = []; //all direct children of the schema @observable _referenceSelectMode: { enabled: boolean; currEditing: SchemaCellField | undefined } = { enabled: false, currEditing: undefined }; // 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 = DocumentView.SelectedDocs().filter(doc => this.docs.includes(doc)); //&& this._selectedCells.includes(doc) if (!selected.length) { // if no schema doc is directly selected, test if a child of a schema doc is selected (such as in the preview window) const childOfSchemaDoc = DocumentView.SelectedDocs().find(sel => DocumentView.getContextPath(sel, true).includes(this.Document)); if (childOfSchemaDoc) { const contextPath = DocumentView.getContextPath(childOfSchemaDoc, true); return [contextPath[contextPath.indexOf(childOfSchemaDoc) - 1]]; // the schema doc that is "selected" by virtue of one of its children being selected } } return selected; } @computed get highlightedCells() { return this._highlightedCellsInfo.map(info => this.getCellElement(info[0], info[1])); } @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 StrListCast(this.layoutDoc.schema_columnKeys, defaultColumnKeys); } @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 (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) { return widths.map(w => (w / totalWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)); } return widths; } @computed get rowHeights() { return this.docs.map(() => this.rowHeightFunc()); } @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 => this.fieldInfos.set(pair[0], pair[1] as FInfo)); this._keysDisposer = observe( this.dataDoc[this.fieldKey ?? 'data'] as List, change => { switch (change.type) { case 'splice': // prettier-ignore change.added.filter(doc => doc instanceof Doc).map(doc => doc as Doc).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 break; default: } }, true ); this._disposers.docdata = reaction( () => DocListCast(this.dataDoc[this.fieldKey]), docs => (this._containedDocs = docs), { fireImmediately: true } ); this._disposers.sortHighlight = reaction( () => [this.sortField, this._containedDocs, this._selectedDocs, this._highlightedCellsInfo], () => { this.sortField && setTimeout(() => this.highlightSortedColumn()); }, { fireImmediately: true } ); } componentWillUnmount() { this._keysDisposer?.(); Object.values(this._disposers).forEach(disposer => disposer?.()); document.removeEventListener('keydown', this.onKeyDown); } // ViewBoxInterface overrides override isUnstyledView = returnTrue; // used by style provider : turns off opacity, animation effects, scaling removeDoc = (doc: Doc) => { this.removeDocument(doc); this._containedDocs = this._containedDocs.filter(d => d !== doc); }; rowIndex = (doc: Doc) => this.docsWithDrag.docs.indexOf(doc); @action onKeyDown = (e: KeyboardEvent) => { if (this._selectedDocs.length > 0) { switch (e.key + (e.shiftKey ? 'Shift' : '')) { case 'Enter': case 'ArrowDown': { const lastDoc = this._selectedDocs.lastElement(); const lastIndex = this.rowIndex(lastDoc); const curDoc = this.docs[lastIndex]; if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) { const newDoc = this.docs[lastIndex + 1]; if (this._selectedDocs.includes(newDoc)) { DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc)); this.deselectCell(curDoc); } else { this.selectCell(newDoc, this._selectedCol, e.shiftKey, e.ctrlKey); this.scrollToDoc(newDoc, {}); } } e.stopPropagation(); e.preventDefault(); } break; case 'EnterShift': case 'ArrowUp': { const firstDoc = this._selectedDocs.lastElement(); const firstIndex = this.rowIndex(firstDoc); const curDoc = this.docs[firstIndex]; if (firstIndex > 0 && firstIndex < this.childDocs.length) { const newDoc = this.docs[firstIndex - 1]; if (this._selectedDocs.includes(newDoc)) { DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc)); this.deselectCell(curDoc); } else { this.selectCell(newDoc, this._selectedCol, e.shiftKey, e.ctrlKey); this.scrollToDoc(newDoc, {}); } } e.stopPropagation(); e.preventDefault(); } break; case 'Tab': 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 'TabShift': 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._selectedDocs.forEach(d => this._containedDocs.includes(d) && this.removeDoc(d)); }, 'delete schema row'); break; } case 'Escape': { this.deselectAllCells(); break; } case 'P': { break; } default: } } }; addRow = (doc: Doc | Doc[]) => this.addDocument(doc); @undoBatch changeColumnKey = (index: number, newKey: string, defaultVal?: FieldType) => { if (!this.documentKeys.includes(newKey)) this.addNewKey(newKey, defaultVal); const currKeys = this.columnKeys.slice(); // copy the column key array first, then change it. currKeys[index] = newKey; this.layoutDoc.schema_columnKeys = new List(currKeys); }; @undoBatch addColumn = (index: number = 0, keyIn?: string, defaultVal?: FieldType) => { let key = keyIn; if (key && !this.documentKeys.includes(key)) this.addNewKey(key, defaultVal); const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1); const currWidths = this.storedColumnWidths.slice(); currWidths.splice(index, 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))); const currKeys = this.columnKeys.slice(); if (!key) key = 'EmptyColumnKey' + Math.floor(Math.random() * 1000000000000000).toString(); currKeys.splice(index, 0, key); this.changeColumnKey(index, 'EmptyColumnKey' + Math.floor(Math.random() * 1000000000000000).toString()); this.layoutDoc.schema_columnKeys = new List(currKeys); }; @action addNewKey = (key: string, defaultVal: FieldType | undefined) => { this.childDocs.forEach(doc => { if (doc[DocData][key] === undefined) doc[DocData][key] = defaultVal; }); }; @undoBatch removeColumn = (index: number) => { if (this.columnKeys.length === 1) return; if (this._columnMenuIndex === index) { this._headerRefs[index].toggleEditing(false); this.closeNewColumnMenu(); } 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))); const currKeys = this.columnKeys.slice(); currKeys.splice(index, 1); this.layoutDoc.schema_columnKeys = new List(currKeys); this._colEles.splice(index, 1); }; @action startResize = (e: React.PointerEvent, index: number, rightSide: boolean) => { this._displayColumnWidths = this.storedColumnWidths; setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index, rightSide), this.finishResize, emptyFunction); }; @action resizeColumn = (e: PointerEvent, index: number, rightSide: boolean) => { if (this._displayColumnWidths) { let shrinking; let growing; let change = e.movementX; if (rightSide && index !== this._displayColumnWidths.length - 1) { growing = change < 0 ? index + 1 : index; shrinking = change < 0 ? index : index + 1; } else 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 const currKeys = this.columnKeys.slice(); currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnKeys = new List(currKeys); const currWidths = this.storedColumnWidths.slice(); currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnWidths = new List(currWidths); }; @action dragColumn = (e: PointerEvent, index: number) => { this.closeNewColumnMenu(); this._headerRefs.forEach(ref => ref.toggleEditing(false)); this._draggedColIndex = index; this.setColDrag(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); return true; }; /** * Uses cursor x coordinate to calculate which index the column should be rendered/dropped in * @param mouseX cursor x coordinate * @returns column index */ findColDropIndex = (mouseX: number) => { const xOffset: number = this._props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[0] + CollectionSchemaView._rowMenuWidth; 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; }, xOffset); return index; }; /** * Calculates the current index of dragged rows for dynamic rendering and drop logic. * @param mouseY user's cursor position relative to the viewport * @returns row index the dragged doc should be rendered/dropped in */ findRowDropIndex = (mouseY: number): number => { const 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; const 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 setRelCursorIndex = (mouseY: number) => { this._mouseCoordinates.y = mouseY; // updates this.rowDropIndex computed value to overwrite the old cached value const rowHeight = CollectionSchemaView._rowHeight; const adjInitMouseY = mouseY - rowHeight - 100; // rowHeight: height of the column menu cells | 100: height of the top menu const 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; }; highlightDraggedColumn = (index: number) => this._colEles.forEach((colRef, i) => { const edgeStyle = i === index ? `solid 2px ${Colors.MEDIUM_BLUE}` : ''; const sorted = i === this.columnKeys.indexOf(this.sortField); const cellEles = [colRef, ...this.docsWithDrag.docs.filter(doc => (i !== this._selectedCol || !this._selectedDocs.includes(doc)) && !sorted).map(doc => this._rowEles.get(doc).children[1].children[i])]; cellEles.forEach(ele => { if (sorted || this.highlightedCells.includes(ele)) return; ele.style.borderTop = ele === cellEles[0] ? edgeStyle : ''; ele.style.borderLeft = edgeStyle; ele.style.borderRight = edgeStyle; ele.style.borderBottom = ele === cellEles.slice(-1)[0] ? edgeStyle : ''; }); }); removeDragHighlight = () => { this._colEles.forEach((colRef, i) => { const sorted = i === this.columnKeys.indexOf(this.sortField); if (sorted) return; colRef.style.borderLeft = ''; colRef.style.borderRight = ''; colRef.style.borderTop = ''; this.childDocs.forEach(doc => { const cell = this._rowEles.get(doc).children[1].children[i]; if (!(this._selectedDocs.includes(doc) && i === this._selectedCol) && !this.highlightedCells.includes(cell) && cell) { cell.style.borderLeft = ''; cell.style.borderRight = ''; cell.style.borderBottom = ''; } }); }); }; /** * Applies a gradient highlight to a sorted column. The direction of the gradient depends * on whether the sort is ascending or descending. * @param field the column being sorted * @param descending whether the sort is descending or ascending; descending if true */ highlightSortedColumn = (field?: string, descending?: boolean) => { let index = -1; const highlightColors: string[] = []; const rowCount: number = this._containedDocs.length + 1; if (field || this.sortField) { index = this.columnKeys.indexOf(field || this.sortField); const increment: number = 110 / rowCount; for (let i = 1; i <= rowCount; ++i) { const adjColor = ClientUtils.lightenRGB(16, 66, 230, increment * i); highlightColors.push(`solid 2px rgb(${adjColor[0]}, ${adjColor[1]}, ${adjColor[2]})`); } } this._colEles.forEach((colRef, i) => { const highlight: boolean = i === index; const desc: boolean = descending || this.sortDesc; const cellEles = [colRef, ...this.docsWithDrag.docs.filter(doc => i !== this._selectedCol || !this._selectedDocs.includes(doc)).map(doc => this._rowEles.get(doc).children[1].children[i])]; const cellCount = cellEles.length; for (let ele = 0; ele < cellCount; ++ele) { const currCell = cellEles[ele]; if (this.highlightedCells.includes(currCell)) continue; const style = highlight ? (desc ? `${highlightColors[cellCount - 1 - ele]}` : `${highlightColors[ele]}`) : ''; currCell.style.borderLeft = style; currCell.style.borderRight = style; } cellEles[0].style.borderTop = highlight ? (desc ? `${highlightColors[cellCount - 1]}` : `${highlightColors[0]}`) : ''; if (!(this._selectedDocs.includes(this.docsWithDrag.docs[this.docsWithDrag.docs.length - 1]) && this._selectedCol === index) && !this.highlightedCells.includes(cellEles[cellCount - 1])) cellEles[cellCount - 1].style.borderBottom = highlight ? (desc ? `${highlightColors[0]}` : `${highlightColors[cellCount - 1]}`) : ''; }); }; /** * Gets the html element representing a cell so that styles can be applied on it. * @param doc the cell's row document * @param fieldKey the cell's column's field key * @returns the html element representing the cell at the given location */ getCellElement = (doc: Doc, fieldKey: string) => { const index = this.columnKeys.indexOf(fieldKey); const cell = this._rowEles.get(doc).children[1].children[index]; return cell; }; /** * Given text in a cell, find references to other cells (for equations). * @param text the text in the cell * @returns the html cell elements referenced in the text. */ findCellRefs = (text: string) => { const pattern = /(this|d(\d+))\.(\w+)/g; interface Match { docRef: string; field: string; } const matches: Match[] = []; let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { const docRef = match[1] === 'this' ? match[1] : match[2]; matches.push({ docRef, field: match[3] }); } const cells: [Doc, string][] = []; matches.forEach((m: Match) => { const { docRef, field } = m; const docView = DocumentManager.Instance.DocumentViews[Number(docRef)]; const doc = docView?.Document ?? undefined; if (this.columnKeys.includes(field) && this._containedDocs.includes(doc)) { cells.push([doc, field]); } }); return cells; }; /** * Determines whether the rows above or below a given row have been * selected, so selection highlights don't overlap. * @param doc the document row to check * @returns a boolean tuple where 0 is the row above, and 1 is the row below */ selectionOverlap = (doc: Doc): [boolean, boolean] => { const docs = this.docsWithDrag.docs; const index = this.rowIndex(doc); const selectedBelow: boolean = this._selectedDocs.includes(docs[index + 1]); const selectedAbove: boolean = this._selectedDocs.includes(docs[index - 1]); return [selectedAbove, selectedBelow]; }; @action removeCellHighlights = () => { this._highlightedCellsInfo.forEach(info => { const doc = info[0]; const field = info[1]; const cell = this.getCellElement(doc, field); if (this._selectedDocs.includes(doc) && this._selectedCol === this.columnKeys.indexOf(field)) { cell.style.border = `solid 2px ${Colors.MEDIUM_BLUE}`; if (this.selectionOverlap(doc)[0]) cell.style.borderTop = ''; if (this.selectionOverlap(doc)[1]) cell.style.borderBottom = ''; } else cell.style.border = ''; cell.style.backgroundColor = ''; }); this._highlightedCellsInfo = []; }; restoreCellHighlights = () => { this._highlightedCellsInfo.forEach(info => { const doc = info[0]; const field = info[1]; const key = `${doc[Id]}_${field}`; const cell = this.getCellElement(doc, field); const color = this._cellHighlightColors.get(key)[0]; cell.style.borderTop = color; cell.style.borderLeft = color; cell.style.borderRight = color; cell.style.borderBottom = color; }); }; /** * Highlights cells based on equation text in the cell currently being edited. * Does not highlight selected cells (that's done directly in SchemaTableCell). * @param text the equation */ highlightCells = (text: string) => { this.removeCellHighlights(); const cellsToHighlight = this.findCellRefs(text); this._highlightedCellsInfo = [...cellsToHighlight]; for (let i = 0; i < this._highlightedCellsInfo.length; ++i) { const info = this._highlightedCellsInfo[i]; const color = this._eqHighlightColors[i % 10]; const colorStrings = [`solid 2px rgb(${color[0].r}, ${color[0].g}, ${color[0].b})`, `rgb(${color[1].r}, ${color[1].g}, ${color[1].b})`]; const doc = info[0]; const field = info[1]; const key = `${doc[Id]}_${field}`; const cell = this.getCellElement(doc, field); this._cellHighlightColors.set(key, [colorStrings[0], colorStrings[1]]); cell.style.border = colorStrings[0]; cell.style.backgroundColor = colorStrings[1]; } }; //Used in SchemaRowBox @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) => { const rowDocView = DocumentView.getDocumentView(doc); if (rowDocView) DocumentView.SelectView(rowDocView, extendSelection); }; @action clearSelection = () => { if (this._referenceSelectMode.enabled) return; DocumentView.DeselectAll(); this.deselectAllCells(); }; selectRow = (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.docsWithDrag.docs[i]; if (!this._selectedDocs.includes(currDoc)) { this.selectCell(currDoc, this._selectedCol, false, true); } } }; //Used in SchemaRowBox selectReference = (doc: Doc | undefined, col: number) => { if (!doc) return; const docIndex = DocumentView.getDocViewIndex(doc); const field = this.columnKeys[col]; const refToAdd = `d${docIndex}.${field}`; const editedField = this._referenceSelectMode.currEditing ? (this._referenceSelectMode.currEditing as SchemaCellField) : null; editedField?.insertText(refToAdd, true); editedField?.setupRefSelect(false); return; }; @action selectCell = (doc: Doc, col: number, shiftKey: boolean, ctrlKey: boolean) => { this.closeNewColumnMenu(); if (!shiftKey && !ctrlKey) this.clearSelection(); !this._selectedCells && (this._selectedCells = []); !shiftKey && this._selectedCells.push(doc); const index = this.rowIndex(doc); if (!this) return; const lastSelected = Array.from(this._selectedDocs).lastElement(); if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRow(doc, lastSelected); else if (ctrlKey) { if (lastSelected && this._selectedDocs.includes(doc)) { DocumentView.DeselectView(DocumentView.getFirstDocumentView(doc)); this.deselectCell(doc); } else this.addDocToSelection(doc, true); } else this.addDocToSelection(doc, false); this._selectedCol = col; if (this._lowestSelectedIndex === -1 || index < this._lowestSelectedIndex) this._lowestSelectedIndex = index; }; @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(d => this.rowIndex(d))); }; @action deselectAllCells = () => { this._selectedCells = []; this._lowestSelectedIndex = -1; }; @computed get rowDropIndex() { const mouseY = this.ScreenToLocalBoxXf().transformPoint(this._mouseCoordinates.x, this._mouseCoordinates.y)[1]; return this.findRowDropIndex(mouseY); } @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.columnDragData) { setTimeout(() => { this.setColDrag(false); }); e.stopPropagation(); return true; } const draggedDocs = de.complete.docDragData?.draggedDocuments; if (draggedDocs && super.onInternalDrop(e, de) && !this.sortField) { const docs = this.docsWithDrag.docs.slice(); this.dataDoc[this.fieldKey ?? 'data'] = new List([...docs]); this.clearSelection(); draggedDocs.forEach(doc => { DocumentView.addViewRenderedCb(doc, dv => dv.select(true)); }); this._lowestSelectedIndex = Math.min(...(draggedDocs?.map(doc => this.rowIndex(doc)) ?? [])); return true; } return false; }; onExternalDrop = (e: React.DragEvent) => super.onExternalDrop(e, {}, docs => docs.map(doc => this.addDocument(doc))); onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction); @action onDividerMove = (e: PointerEvent) => { 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 => 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()) { const focusSpeed = options.zoomTime ?? 50; smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc); return focusSpeed; } } return undefined; }; @action setKey = (key: string, defaultVal?: string, index?: number) => { if (this.columnKeys.includes(key)) return; if (this._makeNewColumn) { this.addColumn(this.columnKeys.indexOf(key), key, defaultVal); this._makeNewColumn = false; } else this.changeColumnKey(this._columnMenuIndex! | index!, key, defaultVal); this.closeNewColumnMenu(); }; /** * Used in SchemaRowBox to set * @param key * @param value * @returns */ setCellValues = (key: string, value: string) => { if (this._selectedCells.length === 1) this.docs.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value)); else this._selectedCells.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value)); return true; }; @action openNewColumnMenu = (index: number, newCol: boolean) => { this.closeFilterMenu(); this._makeNewColumn = false; this._columnMenuIndex = index; this._menuValue = ''; this._menuKeys = this.documentKeys; this._newFieldWarning = ''; this._makeNewColumn = newCol; }; @action closeNewColumnMenu = () => { this._columnMenuIndex = undefined; }; @action openFilterMenu = (index: number) => { this._filterColumnIndex = index; this._filterSearchValue = ''; }; @action closeFilterMenu = () => { this._filterColumnIndex = undefined; }; @undoBatch setColumnSort = (field: string | undefined, desc: boolean = false) => { this.layoutDoc.sortField = field; this.layoutDoc.sortDesc = desc; }; openContextMenu = (x: number, y: number, index: number) => { this.closeNewColumnMenu(); this.closeFilterMenu(); const cm = ContextMenu.Instance; cm.clearItems(); const fieldSortedAsc = this.sortField === this.columnKeys[index] && !this.sortDesc; const fieldSortedDesc = this.sortField === this.columnKeys[index] && this.sortDesc; const revealOptions = cm.findByDescription('Sort column'); const sortOptions: ContextMenuProps[] = revealOptions && revealOptions && 'subitems' in revealOptions ? (revealOptions.subitems ?? []) : []; sortOptions.push({ description: 'Sort A-Z', event: () => { this.setColumnSort(undefined); const field = this.columnKeys[index]; this._containedDocs = this.sortDocs(field, false); setTimeout(() => { this.highlightSortedColumn(field, false); setTimeout(() => this.highlightSortedColumn(), 480); }, 20); }, icon: 'arrow-down-a-z', }); sortOptions.push({ description: 'Sort Z-A', event: () => { this.setColumnSort(undefined); const field = this.columnKeys[index]; this._containedDocs = this.sortDocs(field, true); setTimeout(() => { this.highlightSortedColumn(field, true); setTimeout(() => this.highlightSortedColumn(), 480); }, 20); }, icon: 'arrow-up-z-a', }); sortOptions.push({ description: 'Persistent Sort A-Z', event: () => { if (fieldSortedAsc){ this.setColumnSort(undefined); this.highlightSortedColumn(); } else { this.sortDocs(this.columnKeys[index], false); this.setColumnSort(this.columnKeys[index], false); } }, icon: fieldSortedAsc ? 'lock' : 'lock-open'}); // prettier-ignore sortOptions.push({ description: 'Persistent Sort Z-A', event: () => { if (fieldSortedDesc){ this.setColumnSort(undefined); this.highlightSortedColumn(); } else { this.sortDocs(this.columnKeys[index], true); this.setColumnSort(this.columnKeys[index], true); } }, icon: fieldSortedDesc ? 'lock' : 'lock-open'}); // prettier-ignore cm.addItem({ description: `Change field`, event: () => this.openNewColumnMenu(index, false), icon: 'pencil-alt', }); cm.addItem({ description: 'Filter field', event: () => this.openFilterMenu(index), icon: 'filter', }); cm.addItem({ description: 'Sort column', addDivider: false, noexpand: true, subitems: sortOptions, icon: 'sort', }); cm.addItem({ description: 'Add column to left', event: () => this.addColumn(index), icon: 'plus', }); cm.addItem({ description: 'Add column to right', event: () => this.addColumn(index + 1), icon: 'plus', }); cm.addItem({ description: 'Delete column', event: () => this.removeColumn(index), icon: 'trash', }); cm.displayMenu(x, y, undefined, false); }; //used in schemacolumnheader @action updateKeySearch = (val: string) => { this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(val.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; default: } }; @action updateFilterSearch = (e: React.ChangeEvent) => { this._filterSearchValue = e.target.value; }; 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(); }; setRef = (r: HTMLDivElement | null) => { this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel); this._oldKeysWheel = r; r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false }); }; _oldKeysWheel: HTMLDivElement | null = null; @computed get keysDropdown() { return (
{this._menuKeys.map(key => (
{ e.stopPropagation(); this.setKey(key); }}>

{key} :   {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 (
{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={e => Doc.setDocFilter(this.Document, columnKey, key, e.target.checked ? 'check' : '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 setColDrag = (beingDragged: boolean) => { this._colBeingDragged = beingDragged; !beingDragged && this.removeDragHighlight(); }; @action updateMouseCoordinates = (e: React.PointerEvent) => { const prevX = this._mouseCoordinates.x; const prevY = this._mouseCoordinates.y; this._mouseCoordinates = { x: e.clientX, y: e.clientY, prevX: prevX, prevY: prevY }; }; @action onPointerMove = (e: React.PointerEvent) => { if (DragManager.docsBeingDragged.length) { this.updateMouseCoordinates(e); } if (this._colBeingDragged) { this.updateMouseCoordinates(e); const newIndex = this.findColDropIndex(e.clientX); const direction: number = this._mouseCoordinates.x > this._mouseCoordinates.prevX ? 1 : 0; if (newIndex !== undefined && ((newIndex > this._draggedColIndex && direction === 1) || (newIndex < this._draggedColIndex && direction === 0))) { this.moveColumn(this._draggedColIndex, newIndex ?? this._draggedColIndex); this._draggedColIndex = newIndex !== undefined ? newIndex : this._draggedColIndex; } this.highlightSortedColumn(); //TODO: Make this more efficient this.restoreCellHighlights(); !(this.sortField && this._draggedColIndex === this.columnKeys.indexOf(this.sortField)) && this.highlightDraggedColumn(this._draggedColIndex); } }; /** * Gets docs contained by collections within the schema. Currently defunct. * @param doc * @param displayed * @returns */ // subCollectionDocs = (doc: Doc, displayed: boolean) => { // const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); // let collections: Array = []; // if (displayed) collections = childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema); // else collections = childDocs.filter(d => d.type === 'collection' && !d._childrenSharedWithSchema); // let toReturn: Doc[] = [...childDocs]; // collections.forEach(d => toReturn = toReturn.concat(this.subCollectionDocs(d, displayed))); // return toReturn; // } /** * Applies any filters active on the schema to filter out docs that don't match. */ @computed get filteredDocs() { const childDocFilters = this.childDocFilters(); const childFiltersByRanges = this.childDocRangeFilters(); const searchDocs = this.searchFilterDocs(); const docsforFilter: Doc[] = []; this._containedDocs.forEach(d => { // dragging facets const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter)); if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return; let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0; const fieldKey = Doc.LayoutDataKey(d); const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List)?.some(ele => !(ele instanceof Doc)); const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey]; const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar']; if (docChildDocs !== undefined || sidebarDocs !== undefined) { let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)]; if (subDocs.length > 0) { let newarray: Doc[] = []; notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; // eslint-disable-next-line no-loop-func subDocs.forEach(t => { const docFieldKey = Doc.LayoutDataKey(t); const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List)?.some(ele => !(ele instanceof Doc)); notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc)); isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } } } } notFiltered && docsforFilter.push(d); }); return docsforFilter; } /** * Returns all child docs of the schema and child docs of contained collections that satisfy applied filters. */ @computed get docs() { //let docsFromChildren: Doc[] = []; // Functionality for adding child docs //const displayedCollections = this.childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema); // displayedCollections.forEach(d => { // let docsNotAlreadyDisplayed = this.subCollectionDocs(d, true).filter(dc => !this._containedDocs.includes(dc)); // docsFromChildren = docsFromChildren.concat(docsNotAlreadyDisplayed); // }); return this.filteredDocs; } /** * Sorts docs first alphabetically and then numerically. * @param field the column being sorted * @param desc whether the sort is ascending or descending * @param persistent whether the sort is applied persistently or is one-shot * @returns */ sortDocs = (field: string, desc: boolean, persistent?: boolean) => { const numbers: Doc[] = []; const strings: Doc[] = []; this.docs.forEach(doc => { if (!isNaN(Number(Field.toString(doc[field] as FieldType)))) numbers.push(doc); else strings.push(doc); }); const sortedNums = numbers.sort((numOne, numTwo) => { const numA = Number(Field.toString(numOne[field] as FieldType)); const numB = Number(Field.toString(numTwo[field] as FieldType)); return desc ? numA - numB : numB - numA; }); const collator = new Intl.Collator(undefined, { sensitivity: 'base' }); let sortedStrings; if (!desc) { sortedStrings = strings.slice().sort((docA, docB) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType))); } else sortedStrings = strings.slice().sort((docB, docA) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType))); const sortedDocs = desc ? sortedNums.concat(sortedStrings) : sortedStrings.concat(sortedNums); if (!persistent) this._containedDocs = sortedDocs; return sortedDocs; }; /** * Returns all docs minus those currently being dragged by the user. */ @computed get docsWithDrag() { let docs = this.docs.slice(); if (this.sortField) { const field = StrCast(this.layoutDoc.sortField); const desc = BoolCast(this.layoutDoc.sortDesc); // is this an ascending or descending sort docs = this.sortDocs(field, desc, true); } else { const draggedDocs = this.isContentActive() ? DragManager.docsBeingDragged.filter(doc => !(doc.type === 'fonticonbox')) : []; docs = docs.filter(d => !draggedDocs.includes(d)); docs.splice(this.rowDropIndex, 0, ...draggedDocs); } return { docs }; } rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight); isContentActive = () => this._props.isSelected() || this._props.isContentActive(); screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0); previewWidthFunc = () => this.previewWidth; displayedDocsFunc = () => this.docsWithDrag.docs; setColHdrRef = (r: SchemaColumnHeader | null) => r && this._headerRefs.push(r); setPreviewRef = (r: HTMLDivElement | null) => (this._previewRef = r); render() { return (
this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)} onPointerMove={e => this.onPointerMove(e)} onPointerDown={() => { this.closeNewColumnMenu(); this.setColDrag(false); }}>
this._props.isContentActive() && e.stopPropagation()} ref={ele => this.fixWheelEvents(ele, this._props.isContentActive)}>
} size={Size.XSMALL} color={'black'} onPointerDown={e => setupMoveUpEvents( this, e, returnFalse, emptyFunction, undoable(clickEv => { clickEv.stopPropagation(); this.addColumn(); }, 'add key to schema') ) } />
{this.columnKeys.map((key, index) => ( CollectionSchemaView._minColWidth} //TODO: update Document={this.Document} key={index} columnIndex={index} columnKeys={this.columnKeys} columnWidths={this.displayColumnWidths} setSort={this.setColumnSort} rowHeight={this.rowHeightFunc} removeColumn={this.removeColumn} resizeColumn={this.startResize} openContextMenu={this.openContextMenu} dragColumn={this.dragColumn} setColRef={this.setColRef} isContentActive={this._props.isContentActive} /> ))}
{this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} { // eslint-disable-next-line no-use-before-define { 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 && (
{Array.from(this._selectedDocs).lastElement() && ( )}
)}
); } } interface CollectionSchemaViewDocProps { schema: CollectionSchemaView; index: number; doc: Doc; rowHeight: () => number; } @observer class CollectionSchemaViewDoc extends ObservableReactComponent { constructor(props: CollectionSchemaViewDocProps) { 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 ( ); } } interface CollectionSchemaViewDocsProps { schema: CollectionSchemaView; setRef: (ref: HTMLDivElement | null) => void; childDocs: () => Doc[]; rowHeight: () => number; } @observer class CollectionSchemaViewDocs extends React.Component { render() { return (
{this.props.childDocs().map((doc: Doc, index: number) => (
))}
); } }