diff options
Diffstat (limited to 'src/client/views/collections/collectionSchema/CollectionSchemaView.tsx')
| -rw-r--r-- | src/client/views/collections/collectionSchema/CollectionSchemaView.tsx | 971 |
1 files changed, 632 insertions, 339 deletions
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 8b0639b3b..0076caaf8 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,12 +1,12 @@ /* eslint-disable no-restricted-syntax */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Popup, PopupTrigger, Type } from 'browndash-components'; -import { ObservableMap, action, computed, makeObservable, observable, observe, runInAction } from 'mobx'; +import { IconButton, Popup, PopupTrigger, Size, Type } from 'browndash-components'; +import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, observe, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils'; +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 { Doc, DocListCast, Field, FieldType, IdToDoc, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; @@ -16,7 +16,6 @@ import { DocUtils } from '../../../documents/DocUtils'; import { Docs, DocumentOptions, FInfo } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; -import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, undoable } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; @@ -31,8 +30,23 @@ import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView' import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; +import { ContextMenuProps } from '../../ContextMenuItem'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { SchemaCellField } from './SchemaCellField'; +import { SnappingManager } from '../../../util/SnappingManager'; + +/** + * 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-var-requires const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export const FInfotoColType: { [key: string]: ColumnType } = { @@ -49,16 +63,31 @@ const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', ' @observer export class CollectionSchemaView extends CollectionSubView() { - private _keysDisposer?: () => void; + private _keysDisposer: any; + 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<HTMLDivElement>(); + private _headerRefs: SchemaColumnHeader[] = []; + private _eqHighlightColors: Array<[{r: number, g: number, b: number}, {r: number, g: number, b: number}]> = []; 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): [any, any] => {return [{r: r, g: g, b: 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; @@ -80,15 +109,21 @@ export class CollectionSchemaView extends CollectionSubView() { @observable _newFieldType: ColumnType = ColumnType.Number; @observable _menuValue: string = ''; @observable _filterColumnIndex: number | undefined = undefined; - @observable _filterSearchValue: string = ''; + @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<Doc> = []; - @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; - + @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<Doc, Array<string>>(); + @observable _highlightedCellsInfo: Array<[doc: Doc, field: string]> = []; + @observable _cellHighlightColors: ObservableMap = new ObservableMap<string, string[]>(); + @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; @@ -96,7 +131,8 @@ export class CollectionSchemaView extends CollectionSubView() { @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.childDocs.includes(doc)); + 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)); @@ -108,6 +144,10 @@ export class CollectionSchemaView extends CollectionSubView() { 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()); } @@ -131,7 +171,6 @@ export class CollectionSchemaView extends CollectionSubView() { ); 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)); } @@ -139,7 +178,7 @@ export class CollectionSchemaView extends CollectionSubView() { } @computed get rowHeights() { - return this.childDocs.map(() => this.rowHeightFunc()); + return this.docs.map(() => this.rowHeightFunc()); } @computed get displayColumnWidths() { @@ -177,17 +216,33 @@ export class CollectionSchemaView extends CollectionSubView() { }, 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 - rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc); + 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) => { @@ -197,9 +252,9 @@ export class CollectionSchemaView extends CollectionSubView() { { const lastDoc = this._selectedDocs.lastElement(); const lastIndex = this.rowIndex(lastDoc); - const curDoc = this.sortedDocs.docs[lastIndex]; + const curDoc = this.docs[lastIndex]; if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) { - const newDoc = this.sortedDocs.docs[lastIndex + 1]; + const newDoc = this.docs[lastIndex + 1]; if (this._selectedDocs.includes(newDoc)) { DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc)); this.deselectCell(curDoc); @@ -216,9 +271,9 @@ export class CollectionSchemaView extends CollectionSubView() { { const firstDoc = this._selectedDocs.lastElement(); const firstIndex = this.rowIndex(firstDoc); - const curDoc = this.sortedDocs.docs[firstIndex]; + const curDoc = this.docs[firstIndex]; if (firstIndex > 0 && firstIndex < this.childDocs.length) { - const newDoc = this.sortedDocs.docs[firstIndex - 1]; + const newDoc = this.docs[firstIndex - 1]; if (this._selectedDocs.includes(newDoc)) { DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc)); this.deselectCell(curDoc); @@ -246,34 +301,26 @@ export class CollectionSchemaView extends CollectionSubView() { } break; case 'Backspace': { - undoable(() => this.removeDocument(this._selectedDocs), 'delete schema row'); + 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: } } }; - @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?: string | number | boolean) => { - if (!this.documentKeys.includes(newKey)) { - this.addNewKey(newKey, defaultVal); - } + changeColumnKey = (index: number, newKey: string, defaultVal?: any) => { + 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; @@ -281,31 +328,36 @@ export class CollectionSchemaView extends CollectionSubView() { }; @undoBatch - addColumn = (key: string, defaultVal?: string | number | boolean) => { - if (!this.documentKeys.includes(key)) { - this.addNewKey(key, defaultVal); - } - + addColumn = (index: number = 0, key?: string, defaultVal?: any) => { + 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(0, 0, newColWidth); + currWidths.splice(index, 0, newColWidth); const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); const currKeys = this.columnKeys.slice(); - currKeys.splice(0, 0, key); + 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<string>(currKeys); }; @action - addNewKey = (key: string, defaultVal?: string | number | boolean) => + addNewKey = (key: string, defaultVal: any) => { this.childDocs.forEach(doc => { 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); @@ -313,24 +365,29 @@ export class CollectionSchemaView extends CollectionSubView() { const currKeys = this.columnKeys.slice(); currKeys.splice(index, 1); - this.layoutDoc.schema_columnKeys = new List<string>(currKeys); + this.layoutDoc.schema_columnKeys = new List<string>(currKeys); + + this._colEles.splice(index, 1); }; @action - startResize = (e: React.PointerEvent, index: number) => { + startResize = (e: any, index: number, rightSide: boolean) => { this._displayColumnWidths = this.storedColumnWidths; - setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index), this.finishResize, emptyFunction); + setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index, rightSide), this.finishResize, emptyFunction); }; @action - resizeColumn = (e: PointerEvent, index: number) => { + resizeColumn = (e: PointerEvent, index: number, rightSide: boolean) => { if (this._displayColumnWidths) { let shrinking; let growing; let change = e.movementX; - if (index !== 0) { + 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; } @@ -368,14 +425,14 @@ export class CollectionSchemaView extends CollectionSubView() { const currWidths = this.storedColumnWidths.slice(); currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnWidths = new List<number>(currWidths); - - this._draggedColIndex = toIndex; }; @action dragColumn = (e: PointerEvent, index: number) => { + this.closeNewColumnMenu(); + this._headerRefs.forEach(ref => ref.toggleEditing(false)); this._draggedColIndex = index; - this._colBeingDragged = true; + 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])); @@ -383,7 +440,13 @@ export class CollectionSchemaView extends CollectionSubView() { 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) { @@ -391,16 +454,35 @@ export class CollectionSchemaView extends CollectionSubView() { else index = i + 1; } return total + curr; - }, 2 * CollectionSchemaView._rowMenuWidth); // probably prone to issues; find better implementation (!!!) + }, xOffset); 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 + * 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 @@ -421,43 +503,196 @@ export class CollectionSchemaView extends CollectionSubView() { this._relCursorIndex = index; }; - findRowDropIndex = (mouseY: 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; - }; - 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.childDocs // - .filter(doc => i !== this._selectedCol || !this._selectedDocs.includes(doc)) + ...this.docsWithDrag.docs + .filter(doc => (i !== this._selectedCol || !this._selectedDocs.includes(doc)) && !sorted) .map(doc => this._rowEles.get(doc).children[1].children[i]), ]; - cellEles[0].style.borderTop = edgeStyle; 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 : ''; }); - cellEles.slice(-1)[0].style.borderBottom = 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: Array<any> = []; + matches.forEach((match: Match) => { + const {docRef, field} = match; + 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); @@ -478,33 +713,48 @@ export class CollectionSchemaView extends CollectionSubView() { @action clearSelection = () => { + if (this._referenceSelectMode.enabled) return; DocumentView.DeselectAll(); this.deselectAllCells(); }; - selectRows = (doc: Doc, lastSelected: Doc) => { + + 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.sortedDocs.docs[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 && this._selectedCells.push(doc); + !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.selectRows(doc, lastSelected); + 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)); @@ -514,8 +764,6 @@ export class CollectionSchemaView extends CollectionSubView() { this._selectedCol = col; if (this._lowestSelectedIndex === -1 || index < this._lowestSelectedIndex) this._lowestSelectedIndex = index; - - // let selectedIndexes: Array<Number> = this._selectedCells.map(doc => this.rowIndex(doc)); }; @action @@ -530,41 +778,24 @@ export class CollectionSchemaView extends CollectionSubView() { 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]; return this.findRowDropIndex(mouseY); } + @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.columnDragData) { - this._colBeingDragged = false; + setTimeout(() => {this.setColDrag(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) { - const map = draggedDocs?.map(doc => this.rowIndex(doc)); - console.log(map); - this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...this.sortedDocs.docs]); + const docs = this.docsWithDrag.docs.slice(); + this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...docs]); this.clearSelection(); draggedDocs.forEach(doc => { DocumentView.addViewRenderedCb(doc, dv => dv.select(true)); @@ -617,119 +848,44 @@ export class CollectionSchemaView extends CollectionSubView() { return undefined; }; - @computed get fieldDefaultInput() { - switch (this._newFieldType) { - case ColumnType.Number: - return ( - <input - type="number" - name="" - id="" - value={Number(this._newFieldDefault ?? 0)} - onPointerDown={e => e.stopPropagation()} - onChange={action(e => { - this._newFieldDefault = e.target.value; - })} - /> - ); - case ColumnType.Boolean: - return ( - <> - <input - type="checkbox" - value={this._newFieldDefault?.toString()} - onPointerDown={e => e.stopPropagation()} - onChange={action(e => { - this._newFieldDefault = e.target.checked; - })} - /> - {this._newFieldDefault ? 'true' : 'false'} - </> - ); - case ColumnType.String: - return ( - <input - type="text" - name="" - id="" - value={this._newFieldDefault?.toString() ?? ''} - onPointerDown={e => e.stopPropagation()} - onChange={action(e => { - this._newFieldDefault = e.target.value; - })} - /> - ); - default: - return undefined; - } - } - - onSearchKeyDown = (e: React.KeyboardEvent) => { - switch (e.key) { - case 'Enter': - this._menuKeys.length > 0 && this._menuValue.length > 0 - ? this.setKey(this._menuKeys[0]) - : runInAction(() => { - this._makeNewField = true; - }); - break; - case 'Escape': - this.closeColumnMenu(); - break; - default: - } - }; - @action - setKey = (key: string, defaultVal?: string | number | boolean) => { + setKey = (key: string, defaultVal?: any, index?: number) => { + if (this.columnKeys.includes(key)) return; + if (this._makeNewColumn) { - this.addColumn(key, defaultVal); - } else { - this.changeColumnKey(this._columnMenuIndex!, key, defaultVal); - } - this.closeColumnMenu(); - }; + this.addColumn(this.columnKeys.indexOf(key), key, defaultVal); + this._makeNewColumn = false; + } else this.changeColumnKey(this._columnMenuIndex! | index!, key, defaultVal); - setColumnValues = (key: string, value: string) => { - const selectedDocs: Doc[] = []; - this.childDocs.forEach(doc => { - const docIsSelected = this._selectedCells && !(this._selectedCells?.filter(d => d === doc).length === 0); - if (docIsSelected) { - selectedDocs.push(doc); - } - }); - if (selectedDocs.length === 1) { - this.childDocs.forEach(doc => Doc.SetField(doc, key, value)); - } else { - selectedDocs.forEach(doc => Doc.SetField(doc, key, value)); - } - return true; + this.closeNewColumnMenu(); }; - setSelectedColumnValues = (key: string, value: string) => { - this.childDocs.forEach(doc => { - const docIsSelected = this._selectedCells && !(this._selectedCells?.filter(d => d === doc).length === 0); - if (docIsSelected) { - Doc.SetField(doc, key, value); - } - }); + /** + * 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 - openColumnMenu = (index: number, newCol: boolean) => { + openNewColumnMenu = (index: number, newCol: boolean) => { + this.closeFilterMenu(); + 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 = () => { + closeNewColumnMenu = () => { this._columnMenuIndex = undefined; }; @@ -744,32 +900,110 @@ export class CollectionSchemaView extends CollectionSubView() { 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.closeColumnMenu(); + this.closeNewColumnMenu(); this.closeFilterMenu(); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ - description: 'Change field', - event: () => this.openColumnMenu(index, false), + 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', }); - ContextMenu.Instance.addItem({ + cm.addItem({ description: 'Filter field', event: () => this.openFilterMenu(index), icon: 'filter', }); - ContextMenu.Instance.addItem({ + 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', }); - ContextMenu.Instance.displayMenu(x, y, undefined, false); + cm.displayMenu(x, y, undefined, false); }; + //used in schemacolumnheader @action - updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => { - this._menuValue = e.target.value; - this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); + 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); @@ -793,65 +1027,6 @@ export class CollectionSchemaView extends CollectionSubView() { this._filterSearchValue = e.target.value; }; - @computed get newFieldMenu() { - return ( - <div className="schema-new-key-options"> - <div className="schema-key-type-option"> - <input - type="radio" - name="newFieldType" - checked={this._newFieldType === ColumnType.Number} - onChange={action(() => { - this._newFieldType = ColumnType.Number; - this._newFieldDefault = 0; - })} - /> - number - </div> - <div className="schema-key-type-option"> - <input - type="radio" - name="newFieldType" - checked={this._newFieldType === ColumnType.Boolean} - onChange={action(() => { - this._newFieldType = ColumnType.Boolean; - this._newFieldDefault = false; - })} - /> - boolean - </div> - <div className="schema-key-type-option"> - <input - type="radio" - name="newFieldType" - checked={this._newFieldType === ColumnType.String} - onChange={action(() => { - this._newFieldType = ColumnType.String; - this._newFieldDefault = ''; - })} - /> - string - </div> - <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div> - <div className="schema-key-warning">{this._newFieldWarning}</div> - <div - className="schema-column-menu-button" - onPointerDown={action(() => { - 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 - </div> - </div> - ); - } - 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(); @@ -862,14 +1037,6 @@ export class CollectionSchemaView extends CollectionSubView() { return ( <div className="schema-key-search"> <div - className="schema-column-menu-button" - onPointerDown={action(e => { - e.stopPropagation(); - this._makeNewField = true; - })}> - + new field - </div> - <div className="schema-key-list" ref={r => { this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel); @@ -887,11 +1054,8 @@ export class CollectionSchemaView extends CollectionSubView() { <p> <span className="schema-search-result-key"> <b>{key}</b> - {this.fieldInfos.get(key)!.fieldType ? ':' : ''} - </span> - <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}> - {this.fieldInfos.get(key)!.fieldType} </span> + <span>: </span> <span className="schema-search-result-desc"> {this.fieldInfos.get(key)!.description}</span> </p> </div> @@ -904,17 +1068,8 @@ export class CollectionSchemaView extends CollectionSubView() { @computed get renderColumnMenu() { const x = this._columnMenuIndex! === -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); return ( - <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}> - <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> - {this._makeNewField ? this.newFieldMenu : this.keysDropdown} - </div> - ); - } - get renderKeysMenu() { - return ( - <div className="schema-column-menu" style={{ left: 0, minWidth: CollectionSchemaView._minColWidth }}> - <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> - {this._makeNewField ? this.newFieldMenu : this.keysDropdown} + <div className="schema-column-menu" style={{ left: x, maxWidth: `${Math.max(this._colEles[this._columnMenuIndex ?? 0].offsetWidth, 150)}px` }}> + {this.keysDropdown} </div> ); } @@ -940,7 +1095,7 @@ export class CollectionSchemaView extends CollectionSubView() { } return ( <div key={key} className="schema-filter-option"> - <input // + <input type="checkbox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()} @@ -956,7 +1111,7 @@ export class CollectionSchemaView extends CollectionSubView() { @computed get renderFilterMenu() { const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); return ( - <div className="schema-filter-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}> + <div className="schema-filter-menu" style={{ left: x, maxWidth: `${Math.max(this._colEles[this._columnMenuIndex ?? 0].offsetWidth, 150)}px`}}> <input className="schema-filter-input" type="text" value={this._filterSearchValue} onKeyDown={this.onFilterKeyDown} onChange={this.updateFilterSearch} onPointerDown={e => e.stopPropagation()} /> {this.renderFilterOptions} <div @@ -971,51 +1126,177 @@ export class CollectionSchemaView extends CollectionSubView() { ); } + @action setColDrag = (beingDragged: boolean) => { + this._colBeingDragged = beingDragged; + !beingDragged && this.removeDragHighlight(); + } + + @action updateMouseCoordinates = (e: React.PointerEvent<HTMLDivElement>) => { + 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<HTMLDivElement>) => { if (DragManager.docsBeingDragged.length) { - this._mouseCoordinates = { x: e.clientX, y: e.clientY }; + this.updateMouseCoordinates(e); } if (this._colBeingDragged) { + this.updateMouseCoordinates(e); const newIndex = this.findColDropIndex(e.clientX); - if (newIndex !== this._draggedColIndex) this.moveColumn(this._draggedColIndex, newIndex ?? this._draggedColIndex); - this._draggedColIndex = newIndex || this._draggedColIndex; - this.highlightDraggedColumn(newIndex ?? this._draggedColIndex); + 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); } }; - @computed get sortedDocs() { - const draggedDocs = this.isContentActive() ? DragManager.docsBeingDragged : []; - 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 => !draggedDocs.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 FieldType); - const bStr = Field.toString(docB[field] as FieldType); - let out = 0; - if (aStr < bStr) out = -1; - if (aStr > bStr) out = 1; - if (desc) out *= -1; - return out; - }); - - docs.splice(this.rowDropIndex, 0, ...draggedDocs); + /** + * 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<Doc> = []; + // 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.LayoutFieldKey(d); + const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.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.LayoutFieldKey(t); + const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.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); - 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: HTMLDivElement | null = null; + displayedDocsFunc = () => this.docsWithDrag.docs; + _oldWheel: any; render() { return ( - <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)} onPointerMove={e => this.onPointerMove(e)}> + <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} + onDrop={this.onExternalDrop.bind(this)} + onPointerMove={e => this.onPointerMove(e)} + onPointerDown={() => {this.closeNewColumnMenu(); this.setColDrag(false)}}> <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }} /> <div className="schema-table" @@ -1028,26 +1309,38 @@ export class CollectionSchemaView extends CollectionSubView() { }}> <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}> <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}> - <Popup - placement="right" - background={SettingsManager.userBackgroundColor} - color={SettingsManager.userColor} - toggle={<FontAwesomeIcon onPointerDown={() => this.openColumnMenu(-1, true)} icon="plus" />} - trigger={PopupTrigger.CLICK} - type={Type.TERT} - isOpen={this._columnMenuIndex !== -1 ? false : undefined} - popup={this.renderKeysMenu} + <IconButton + tooltip="Add a new key" + icon={ <FontAwesomeIcon icon="plus" size='lg'/>} + size={Size.XSMALL} + color={'black'} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + this.addColumn() + }, 'add key to schema') + ) + } /> </div> {this.columnKeys.map((key, index) => ( <SchemaColumnHeader // eslint-disable-next-line react/no-array-index-key + //cleanupField={this.cleanupComputedField} + ref={r => r && this._headerRefs.push(r)} + keysDropdown={(this.keysDropdown)} + schemaView={this} + columnWidth={() => CollectionSchemaView._minColWidth} //TODO: update + Document={this.Document} key={index} columnIndex={index} columnKeys={this.columnKeys} columnWidths={this.displayColumnWidths} - sortField={this.sortField} - sortDesc={this.sortDesc} setSort={this.setColumnSort} rowHeight={this.rowHeightFunc} removeColumn={this.removeColumn} @@ -1065,7 +1358,7 @@ export class CollectionSchemaView extends CollectionSubView() { // eslint-disable-next-line no-use-before-define <CollectionSchemaViewDocs schema={this} - childDocs={this.sortedDocsFunc} + childDocs={this.displayedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => { this._tableContentRef = ref; @@ -1188,7 +1481,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV interface CollectionSchemaViewDocsProps { schema: CollectionSchemaView; setRef: (ref: HTMLDivElement | null) => void; - childDocs: () => { docs: Doc[] }; + childDocs: () => Doc[]; rowHeight: () => number; } @@ -1197,7 +1490,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP render() { return ( <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}> - {this.props.childDocs().docs.map((doc: Doc, index: number) => ( + {this.props.childDocs().map((doc: Doc, index: number) => ( <div key={doc[Id]} className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}> <CollectionSchemaViewDoc doc={doc} schema={this.props.schema} index={index} rowHeight={this.props.rowHeight} /> </div> @@ -1205,4 +1498,4 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP </div> ); } -} +}
\ No newline at end of file |
