aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/collectionSchema/CollectionSchemaView.tsx')
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx971
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">&nbsp;&nbsp;{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