diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/ColumnMenu.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/ColumnMenu.tsx index 46b557bdfa..3d0c931751 100644 --- a/plugins/spreadsheet-view/src/SpreadsheetView/components/ColumnMenu.tsx +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/ColumnMenu.tsx @@ -3,7 +3,6 @@ import { observer } from 'mobx-react' import { iterMap } from '@jbrowse/core/util' import { Menu } from '@jbrowse/core/ui' import { MenuItem } from '@jbrowse/core/ui/Menu' -import { SvgIcon } from '@mui/material' import { SpreadsheetModel } from '../models/Spreadsheet' import { SpreadsheetViewModel } from '../models/SpreadsheetView' @@ -35,24 +34,24 @@ const ColumnMenu = observer(function ({ ]) } - const filterMenuClick = () => { - viewModel.filterControls.addBlankColumnFilter(columnNumber) - } - const { dataTypeChoices } = spreadsheetModel // make a Map of categoryName => [entry...] - const dataTypeTopLevelMenu = new Map() + type Record = (typeof dataTypeChoices)[0] + type RecordGroup = { isCategory: boolean; subMenuItems: Record[] } + const dataTypeTopLevelMenu = new Map() dataTypeChoices.forEach(dataTypeRecord => { const { displayName, categoryName } = dataTypeRecord if (categoryName) { - if (!dataTypeTopLevelMenu.has(categoryName)) { - dataTypeTopLevelMenu.set(categoryName, { + let entry = dataTypeTopLevelMenu.get(categoryName) as RecordGroup + if (!entry) { + entry = { isCategory: true, subMenuItems: [], - }) + } + dataTypeTopLevelMenu.set(categoryName, entry) } - dataTypeTopLevelMenu.get(categoryName).subMenuItems.push(dataTypeRecord) + entry.subMenuItems.push(dataTypeRecord) } else { dataTypeTopLevelMenu.set(displayName, dataTypeRecord) } @@ -60,30 +59,20 @@ const ColumnMenu = observer(function ({ const { columns, sortColumns } = spreadsheetModel const dataType = currentColumnMenu && columns[columnNumber].dataType - const dataTypeName = (dataType && dataType.type) || '' + const dataTypeName = dataType?.type || '' const dataTypeDisplayName = (currentColumnMenu && columns[columnNumber].dataType.displayName) || '' - const isSortingAscending = Boolean( - sortColumns.length > 0 && - currentColumnMenu && - sortColumns.some( - col => - col.columnNumber === currentColumnMenu.colNumber && !col.descending, - ), - ) - const isSortingDescending = Boolean( - sortColumns.length > 0 && - currentColumnMenu && - sortColumns.some( - col => - col.columnNumber === currentColumnMenu.colNumber && col.descending, - ), - ) - function stopSortingClick() { - columnMenuClose() - spreadsheetModel.setSortColumns([]) - } + const isSortingAscending = + !!currentColumnMenu && + sortColumns.some( + c => c.columnNumber === currentColumnMenu.colNumber && !c.descending, + ) + const isSortingDescending = + !!currentColumnMenu && + sortColumns.some( + c => c.columnNumber === currentColumnMenu.colNumber && c.descending, + ) const menuItems = [ // top-level column menu @@ -92,18 +81,21 @@ const ColumnMenu = observer(function ({ icon: SortIcon, type: 'radio', checked: isSortingAscending, - onClick: isSortingAscending - ? stopSortingClick - : sortMenuClick.bind(null, false), + onClick: () => sortMenuClick(false), }, { label: 'Sort descending', icon: SortIcon, type: 'radio', checked: isSortingDescending, - onClick: isSortingDescending - ? stopSortingClick - : sortMenuClick.bind(null, true), + onClick: () => sortMenuClick(true), + }, + { + label: 'No sort', + icon: SortIcon, + type: 'radio', + checked: !isSortingDescending && !isSortingAscending, + onClick: () => spreadsheetModel.setSortColumns([]), }, // data type menu { @@ -112,46 +104,31 @@ const ColumnMenu = observer(function ({ subMenu: iterMap( dataTypeTopLevelMenu.entries(), ([displayName, record]) => { - const { subMenuItems, typeName } = record - if (typeName) { - const menuEntry = { + if ('typeName' in record && record.typeName) { + const { typeName } = record + return { label: displayName || typeName, - icon: undefined as typeof SvgIcon | undefined, - onClick: () => { - spreadsheetModel.setColumnType(columnNumber, typeName) - }, - } - if (dataTypeName === typeName) { - menuEntry.icon = CheckIcon + icon: dataTypeName === typeName ? CheckIcon : undefined, + onClick: () => + spreadsheetModel.setColumnType(columnNumber, typeName), } - return menuEntry - } - if (subMenuItems) { + } else if ('subMenuItems' in record && record.subMenuItems) { + const { subMenuItems } = record return { label: displayName, - icon: subMenuItems.some( - (i: { typeName: string }) => i.typeName === dataTypeName, - ) + icon: subMenuItems.some(i => i.typeName === dataTypeName) ? CheckIcon : undefined, - subMenu: subMenuItems.map( - ({ - typeName: subTypeName, - displayName: subDisplayName, - }: { - typeName: string - displayName: string - }) => ({ - label: subDisplayName, - icon: subTypeName === dataTypeName ? CheckIcon : undefined, - onClick: () => { - spreadsheetModel.setColumnType(columnNumber, subTypeName) - }, - }), - ), + subMenu: subMenuItems.map(({ typeName, displayName }) => ({ + label: displayName, + icon: typeName === dataTypeName ? CheckIcon : undefined, + onClick: () => + spreadsheetModel.setColumnType(columnNumber, typeName), + })), } + } else { + return null } - return null }, ).filter(Boolean), }, @@ -159,11 +136,12 @@ const ColumnMenu = observer(function ({ // don't display the filter item if this data type doesn't have filtering // implemented - if (dataType && dataType.hasFilter) { + if (dataType?.hasFilter) { menuItems.push({ label: 'Create filter', icon: FilterListIcon, - onClick: filterMenuClick.bind(null, true), + onClick: () => + viewModel.filterControls.addBlankColumnFilter(columnNumber), }) } diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/DataRow.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/DataRow.tsx index 78fb0b71fb..5ea3c1703b 100644 --- a/plugins/spreadsheet-view/src/SpreadsheetView/components/DataRow.tsx +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/DataRow.tsx @@ -16,51 +16,47 @@ import CellData from './CellData' type SpreadsheetModel = Instance type RowModel = Instance -const useStyles = makeStyles()(theme => { - const { palette } = theme - return { - rowNumCell: { - textAlign: 'left', - border: `1px solid ${palette.action.disabledBackground}`, - position: 'relative', - padding: '0 2px 0 0', - whiteSpace: 'nowrap', - userSelect: 'none', - }, - rowNumber: { - fontWeight: 'normal', - display: 'inline-block', - flex: 'none', - paddingRight: '20px', - margin: 0, - whiteSpace: 'nowrap', - }, - rowMenuButton: { - padding: 0, - margin: 0, - position: 'absolute', - right: 0, - display: 'inline-block', - whiteSpace: 'nowrap', - flex: 'none', - }, - rowMenuButtonIcon: {}, - rowSelector: { - position: 'relative', - top: '-2px', - margin: 0, - padding: '0 0.2rem', - }, +const useStyles = makeStyles()(theme => ({ + rowNumCell: { + textAlign: 'left', + border: `1px solid ${theme.palette.action.disabledBackground}`, + position: 'relative', + padding: '0 2px 0 0', + whiteSpace: 'nowrap', + userSelect: 'none', + }, + rowNumber: { + fontWeight: 'normal', + display: 'inline-block', + flex: 'none', + paddingRight: '20px', + margin: 0, + whiteSpace: 'nowrap', + }, + rowMenuButton: { + padding: 0, + margin: 0, + position: 'absolute', + right: 0, + display: 'inline-block', + whiteSpace: 'nowrap', + flex: 'none', + }, + rowMenuButtonIcon: {}, + rowSelector: { + position: 'relative', + top: '-2px', + margin: 0, + padding: '0 0.2rem', + }, - dataRowSelected: { + dataRowSelected: { + background: indigo[100], + '& th': { background: indigo[100], - '& th': { - background: indigo[100], - }, }, - emptyMessage: { captionSide: 'bottom' }, - } -}) + }, +})) const DataRow = observer(function ({ rowModel, @@ -86,7 +82,7 @@ const DataRow = observer(function ({ return ( - + {hideRowSelection ? ( - + {columnDisplayOrder.map(colNumber => ( type RowModel = Instance -interface ColMenu { - colNumber: number - anchorEl: HTMLElement -} - const useStyles = makeStyles()(theme => ({ dataTable: { borderCollapse: 'collapse', - borderSpacing: 0, - boxSizing: 'border-box', '& td': { border: `1px solid ${theme.palette.action.disabledBackground}`, padding: '0.2rem', @@ -38,38 +24,10 @@ const useStyles = makeStyles()(theme => ({ textOverflow: 'ellipsis', }, }, - columnHead: { - fontWeight: 'normal', - border: `1px solid ${theme.palette.action.disabledBackground}`, - position: 'sticky', - top: '-1px', - zIndex: 2, - whiteSpace: 'nowrap', - }, - columnButtonContainer: { - display: 'none', - position: 'absolute', - right: 0, - top: 0, - background: theme.palette.action.disabled, - height: '100%', - boxSizing: 'border-box', - borderLeft: `1px solid ${theme.palette.action.disabledBackground}`, - }, - columnButton: { - padding: 0, - }, - topLeftCorner: { - background: theme.palette.action.disabledBackground, - position: 'sticky', - top: '-1px', - zIndex: 2, - minWidth: theme.spacing(2), - textAlign: 'left', + emptyMessage: { + captionSide: 'bottom', }, - - emptyMessage: { captionSide: 'bottom' }, })) const DataTableBody = observer(function ({ @@ -106,88 +64,14 @@ const DataTable = observer(function ({ page: number rowsPerPage: number }) { - const { columnDisplayOrder, columns, hasColumnNames, rowSet } = model + const { rowSet } = model const { classes } = useStyles() - - // column menu active state - const [currentColumnMenu, setColumnMenu] = useState() - function columnButtonClick( - colNumber: number, - evt: React.MouseEvent, - ) { - setColumnMenu({ - colNumber, - anchorEl: evt.currentTarget, - }) - } - - // column header hover state - const [currentHoveredColumn, setHoveredColumn] = useState() - function columnHeaderMouseOver(colNumber: number) { - setHoveredColumn(colNumber) - } - function columnHeaderMouseOut() { - setHoveredColumn(undefined) - } - - const totalRows = rowSet.count const rows = rowSet.sortedFilteredRows - return ( <> - - - - - {columnDisplayOrder.map(colNumber => ( - - ))} - - + {!rows.length ? ( ) : null}
- - - model.unselectAll()} - disabled={!rowSet.selectedCount} - > - - - - - - - {(hasColumnNames && columns[colNumber]?.name) || - numToColName(colNumber)} -
- - - -
-
- {totalRows ? 'no rows match criteria' : 'no rows present'} + {rowSet.count ? 'no rows match criteria' : 'no rows present'}
diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/DataTableHeader.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/DataTableHeader.tsx new file mode 100644 index 0000000000..c770d80daa --- /dev/null +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/DataTableHeader.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react' +import { IconButton, Tooltip } from '@mui/material' +import { observer } from 'mobx-react' +import { getParent, Instance } from 'mobx-state-tree' +import { makeStyles } from 'tss-react/mui' + +// icons +import CropFreeIcon from '@mui/icons-material/CropFree' +import ArrowDropDown from '@mui/icons-material/ArrowDropDown' + +// locals +import SpreadsheetStateModel from '../models/Spreadsheet' +import ColumnMenu from './ColumnMenu' +import SortIndicator from './SortIndicator' +import { numToColName } from './util' + +type SpreadsheetModel = Instance + +interface ColMenu { + colNumber: number + anchorEl: HTMLElement +} + +const useStyles = makeStyles()(theme => ({ + columnHead: { + fontWeight: 'normal', + background: theme.palette.mode === 'dark' ? '#333' : '#eee', + position: 'sticky', + top: 0, + zIndex: 2, + whiteSpace: 'nowrap', + }, + + columnButtonContainer: { + display: 'none', + position: 'absolute', + right: 0, + top: 0, + background: theme.palette.background.paper, + height: '100%', + }, + + topLeftCorner: { + background: theme.palette.mode === 'dark' ? '#333' : '#eee', + position: 'sticky', + top: 0, + minWidth: theme.spacing(2), + textAlign: 'left', + }, +})) + +const DataTableHeader = observer(function ({ + model, +}: { + model: SpreadsheetModel +}) { + const { classes } = useStyles() + const { columnDisplayOrder, columns, hasColumnNames, rowSet } = model + const [currentColumnMenu, setColumnMenu] = useState() + const [currentHoveredColumn, setHoveredColumn] = useState() + + return ( + <> + + + + + + model.unselectAll()} + disabled={!rowSet.selectedCount} + > + + + + + + {columnDisplayOrder.map(colNumber => ( + setHoveredColumn(colNumber)} + onMouseOut={() => setHoveredColumn(undefined)} + > + + {(hasColumnNames && columns[colNumber]?.name) || + numToColName(colNumber)} +
+ ) => { + setColumnMenu({ + colNumber, + anchorEl: evt.currentTarget, + }) + }} + > + + +
+ + ))} + + + + + ) +}) + +export default DataTableHeader diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/GlobalFilterControls.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/GlobalFilterControls.tsx index eeb2098c5d..1dc44e13fa 100644 --- a/plugins/spreadsheet-view/src/SpreadsheetView/components/GlobalFilterControls.tsx +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/GlobalFilterControls.tsx @@ -14,54 +14,52 @@ const useStyles = makeStyles()({ }, }) -const TextFilter = observer( - ({ - textFilter, - }: { - textFilter: { stringToFind: string; setString: (arg: string) => void } - }) => { - const { classes } = useStyles() - // this paragraph is silliness to debounce the text filter input - const [textFilterValue, setTextFilterValue] = useState( - textFilter.stringToFind, - ) - const debouncedTextFilter = useDebounce(textFilterValue, 500) - useEffect(() => { - textFilter.setString(debouncedTextFilter) - }, [debouncedTextFilter, textFilter]) +const TextFilter = observer(function ({ + textFilter, +}: { + textFilter: { stringToFind: string; setString: (arg: string) => void } +}) { + const { classes } = useStyles() + // this paragraph is silliness to debounce the text filter input + const [textFilterValue, setTextFilterValue] = useState( + textFilter.stringToFind, + ) + const debouncedTextFilter = useDebounce(textFilterValue, 500) + useEffect(() => { + textFilter.setString(debouncedTextFilter) + }, [debouncedTextFilter, textFilter]) - return ( -
- setTextFilterValue(evt.target.value)} - variant="outlined" - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: ( - + setTextFilterValue(evt.target.value)} + variant="outlined" + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + setTextFilterValue('')} > - setTextFilterValue('')} - > - - - - ), - }} - /> -
- ) - }, -) + + + + ), + }} + /> + + ) +}) // eslint-disable-next-line @typescript-eslint/no-explicit-any const GlobalFilterControls = observer(({ model }: { model: any }) => { diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/ImportWizard.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/ImportWizard.tsx index 37d0a386d9..3ee7dd8a1a 100644 --- a/plugins/spreadsheet-view/src/SpreadsheetView/components/ImportWizard.tsx +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/ImportWizard.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' - import { FormControl, FormGroup, @@ -13,11 +12,12 @@ import { Grid, } from '@mui/material' import { makeStyles } from 'tss-react/mui' - import { observer } from 'mobx-react' import { getRoot } from 'mobx-state-tree' import { AbstractRootModel, getSession } from '@jbrowse/core/util' import { FileSelector, ErrorMessage, AssemblySelector } from '@jbrowse/core/ui' + +// locals import { ImportWizardModel } from '../models/ImportWizard' import NumberEditor from './NumberEditor' @@ -25,6 +25,13 @@ const useStyles = makeStyles()(theme => ({ buttonContainer: { marginTop: theme.spacing(1), }, + grid: { + width: '25rem', + margin: '0 auto', + }, + container: { + margin: theme.spacing(2), + }, })) const ImportWizard = observer(({ model }: { model: ImportWizardModel }) => { @@ -46,10 +53,10 @@ const ImportWizard = observer(({ model }: { model: ImportWizardModel }) => { const rootModel = getRoot(model) return ( - + {err ? : null} { File Type - {fileTypes.map(fileTypeName => { - return ( - - model.setFileType(fileTypeName)} - control={} - label={fileTypeName} - /> - - ) - })} + {fileTypes.map(fileTypeName => ( + + model.setFileType(fileTypeName)} + control={} + label={fileTypeName} + /> + + ))} diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/NumberEditor.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/NumberEditor.tsx index 806da46f41..61107bbb72 100644 --- a/plugins/spreadsheet-view/src/SpreadsheetView/components/NumberEditor.tsx +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/NumberEditor.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react' - import { TextField } from '@mui/material' import { makeStyles } from 'tss-react/mui' import { observer } from 'mobx-react' + +// locals import { ImportWizardModel } from '../models/ImportWizard' const useStyles = makeStyles()({ diff --git a/plugins/spreadsheet-view/src/SpreadsheetView/components/SpreadsheetView.tsx b/plugins/spreadsheet-view/src/SpreadsheetView/components/SpreadsheetView.tsx index c185bbcc2f..b27b190ff0 100644 --- a/plugins/spreadsheet-view/src/SpreadsheetView/components/SpreadsheetView.tsx +++ b/plugins/spreadsheet-view/src/SpreadsheetView/components/SpreadsheetView.tsx @@ -11,11 +11,10 @@ import FolderOpenIcon from '@mui/icons-material/FolderOpen' // locals import ImportWizard from './ImportWizard' import Spreadsheet from './Spreadsheet' -import SpreadsheetStateModel from '../models/Spreadsheet' -import SpreadsheetStateViewModel from '../models/SpreadsheetView' - import GlobalFilterControls from './GlobalFilterControls' import ColumnFilterControls from './ColumnFilterControls' +import SpreadsheetStateModel from '../models/Spreadsheet' +import SpreadsheetStateViewModel from '../models/SpreadsheetView' type SpreadsheetModel = Instance type SpreadsheetViewModel = Instance @@ -187,11 +186,7 @@ function StatusBar({ ) } -const SpreadsheetView = observer(function ({ - model, -}: { - model: SpreadsheetViewModel -}) { +export default observer(function ({ model }: { model: SpreadsheetViewModel }) { const { classes } = useStyles() const { spreadsheet, @@ -212,11 +207,7 @@ const SpreadsheetView = observer(function ({ const hide2 = mode !== 'display' || hideFilterControls return ( -
+
{!hide1 || !hide2 ? ( {hide1 ? null : ( @@ -288,5 +279,3 @@ const SpreadsheetView = observer(function ({
) }) - -export default SpreadsheetView