diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx index 9793cc4b4..c5fbb85ab 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx @@ -47,6 +47,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay( setCollaboratorCanvas, setOverlayCanvas, setTheme, + tabularEditor, } = model const { classes } = useStyles() const lgv = getContainingView(model) as unknown as LinearGenomeViewModel @@ -108,6 +109,9 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay( onMouseLeave={onMouseLeave} onMouseDown={onMouseDown} onMouseUp={onMouseUp} + onClick={() => { + tabularEditor.showPane() + }} className={classes.canvas} style={{ cursor: cursor ?? 'default' }} /> diff --git a/packages/jbrowse-plugin-apollo/src/TabularEditor/DataGrid.tsx b/packages/jbrowse-plugin-apollo/src/TabularEditor/DataGrid.tsx deleted file mode 100644 index 133b53204..000000000 --- a/packages/jbrowse-plugin-apollo/src/TabularEditor/DataGrid.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { AppRootModel, getSession } from '@jbrowse/core/util' -import { Autocomplete, TextField } from '@mui/material' -import { - GridCellEditStartParams, - GridColDef, - GridRenderEditCellParams, - GridRowModel, - MuiBaseEvent, - DataGrid as MuiDataGrid, - useGridApiContext, - useGridApiRef, -} from '@mui/x-data-grid' -import { AnnotationFeatureI } from 'apollo-mst' -import { - LocationEndChange, - LocationStartChange, - TypeChange, -} from 'apollo-shared' -import { observer } from 'mobx-react' -import { getRoot, getSnapshot } from 'mobx-state-tree' -import React, { useEffect, useMemo, useState } from 'react' - -import { ApolloInternetAccountModel } from '../ApolloInternetAccount/model' -import { ModifyFeatureAttribute } from '../components/ModifyFeatureAttribute' -import { LinearApolloDisplay } from '../LinearApolloDisplay/stateModel' -import { createFetchErrorMessage } from '../util' - -interface GridRow { - id: string - type: string - refSeq: string - start: number - end: number - feature: AnnotationFeatureI - model: LinearApolloDisplay - attributes: unknown -} - -function getFeatureColumns( - editable: boolean, - internetAccount: ApolloInternetAccountModel, -): GridColDef[] { - return [ - { - field: 'type', - headerName: 'Type', - width: 200, - editable, - renderEditCell: (params: GridRenderEditCellParams) => ( - - ), - }, - { field: 'refSeq', headerName: 'Ref Name', width: 80 }, - { - field: 'start', - headerName: 'Start', - type: 'number', - width: 80, - editable, - }, - { field: 'end', headerName: 'End', type: 'number', width: 80, editable }, - { field: 'attributes', headerName: 'Attributes', width: 300, editable }, - ] -} - -interface AutocompleteInputCellProps extends GridRenderEditCellParams { - internetAccount: ApolloInternetAccountModel -} - -function AutocompleteInputCell(props: AutocompleteInputCellProps) { - const { field, id, internetAccount, row, value } = props - const [soSequenceTerms, setSOSequenceTerms] = useState([]) - const [errorMessage, setErrorMessage] = useState('') - const apiRef = useGridApiContext() - - useEffect(() => { - const controller = new AbortController() - const { signal } = controller - async function getSOSequenceTerms() { - const { feature } = row - const { children, parent, type } = feature - let endpoint = '/ontologies/equivalents/sequence_feature' - if (parent) { - endpoint = `/ontologies/descendants/${parent.type}` - } else if (children?.size) { - endpoint = `/ontologies/equivalents/${type}` - } - const { baseURL, getFetcher } = internetAccount - const uri = new URL(endpoint, baseURL).href - const apolloFetch = getFetcher({ locationType: 'UriLocation', uri }) - const response = await apolloFetch(uri, { method: 'GET', signal }) - if (!response.ok) { - const newErrorMessage = await createFetchErrorMessage( - response, - 'Error when retrieving ontologies from server', - ) - throw new Error(newErrorMessage) - } - const soTerms = (await response.json()) as string[] | undefined - if (soTerms && !signal.aborted) { - setSOSequenceTerms(soTerms) - } - } - getSOSequenceTerms().catch((error) => { - if (!signal.aborted) { - setErrorMessage(String(error)) - } - }) - return () => { - controller.abort() - } - }, [internetAccount, row]) - - const handleChange = async ( - event: MuiBaseEvent, - newValue?: string | null, - ) => { - const isValid = await apiRef.current.setEditCellValue({ - id, - field, - value: newValue, - }) - if (isValid) { - apiRef.current.stopCellEditMode({ id, field }) - } - } - - if (soSequenceTerms.length === 0) { - return null - } - - const extraTextFieldParams: { error?: boolean; helperText?: string } = {} - if (errorMessage) { - extraTextFieldParams.error = true - extraTextFieldParams.helperText = errorMessage - } - - return ( - { - return ( - - ) - }} - value={String(value)} - onChange={handleChange} - disableClearable - selectOnFocus - handleHomeEndKeys - /> - ) -} - -function DataGrid({ model }: { model: LinearApolloDisplay }) { - const session = getSession(model) - const apiRef = useGridApiRef() - const { internetAccounts } = getRoot(session) as AppRootModel - const internetAccount = useMemo(() => { - const apolloInternetAccount = internetAccounts.find( - (ia) => ia.type === 'ApolloInternetAccount', - ) as ApolloInternetAccountModel | undefined - if (!apolloInternetAccount) { - throw new Error('No Apollo internet account found') - } - return apolloInternetAccount - }, [internetAccounts]) - const editable = - Boolean(internetAccount.authType) && - ['admin', 'user'].includes(internetAccount.getRole() ?? '') - const { changeManager, detailsHeight, selectedFeature } = model - if (!selectedFeature) { - return null - } - const { - _id: id, - assemblyId: assembly, - end, - refSeq, - start, - type, - } = selectedFeature - const { assemblyManager } = session - const refName = - assemblyManager.get(assembly)?.getCanonicalRefName(refSeq) ?? refSeq - - let tmp = Object.fromEntries( - [...selectedFeature.attributes.entries()].map(([key, value]) => { - if (key.startsWith('gff_')) { - const newKey = key.slice(4) - const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1) - return [capitalizedKey, getSnapshot(value)] - } - if (key === '_id') { - return ['ID', getSnapshot(value)] - } - return [key, getSnapshot(value)] - }), - ) - let attributes = Object.entries(tmp) - .map(([key, values]) => `${key}=${values.join(', ')}`) - .join(', ') - - const selectedFeatureRows: GridRow[] = [ - { - id, - type, - refSeq: refName, - start, - end, - feature: selectedFeature, - model, - attributes, - }, - ] - function addChildFeatures(f: typeof selectedFeature) { - for (const [, child] of f?.children ?? new Map()) { - tmp = Object.fromEntries( - [...child.attributes.entries()].map(([key, value]) => { - if (key.startsWith('gff_')) { - const newKey = key.slice(4) - const capitalizedKey = - newKey.charAt(0).toUpperCase() + newKey.slice(1) - return [capitalizedKey, getSnapshot(value)] - } - if (key === '_id') { - return ['ID', getSnapshot(value)] - } - return [key, getSnapshot(value)] - }), - ) - attributes = Object.entries(tmp) - .map(([key, values]) => `${key}=${values.join(', ')}`) - .toString() - - selectedFeatureRows.push({ - id: child._id, - type: child.type, - refSeq: refName, - start: child.start, - end: child.end, - feature: child, - model, - attributes, - }) - addChildFeatures(child) - } - } - addChildFeatures(selectedFeature) - function processRowUpdate( - newRow: GridRowModel<(typeof selectedFeatureRows)[0]>, - oldRow: GridRowModel<(typeof selectedFeatureRows)[0]>, - ) { - let change: LocationStartChange | LocationEndChange | TypeChange | undefined - - if (newRow.start !== oldRow.start) { - const { id: featureId, start: oldStart } = oldRow - const { start: newStart } = newRow - change = new LocationStartChange({ - typeName: 'LocationStartChange', - changedIds: [featureId], - featureId, - oldStart, - newStart: Number(newStart), - assembly, - }) - } else if (newRow.end !== oldRow.end) { - const { end: oldEnd, id: featureId } = oldRow - const { end: newEnd } = newRow - change = new LocationEndChange({ - typeName: 'LocationEndChange', - changedIds: [featureId], - featureId, - oldEnd, - newEnd: Number(newEnd), - assembly, - }) - } else if (newRow.type !== oldRow.type) { - const { id: featureId, type: oldType } = oldRow - const { type: newType } = newRow - change = new TypeChange({ - typeName: 'TypeChange', - changedIds: [featureId], - featureId, - oldType: String(oldType), - newType: String(newType), - assembly, - }) - } - if (change) { - changeManager?.submit(change) - } - return newRow - } - return ( - ) => { - if (params.colDef.field !== 'attributes' || !selectedFeature) { - return - } - const { assemblyId } = selectedFeature - session.queueDialog((doneCallback) => [ - ModifyFeatureAttribute, - { - session, - handleClose: doneCallback, - changeManager, - sourceFeature: params.row.feature, - sourceAssemblyId: assemblyId, - }, - ]) - // Without this, `stopCellEditMode` doesn't work because the cell - // is still in view mode. Probably an MUI bug, but since we're - // likely going to replace DataGrid, it's not worth fixing now. - await new Promise((resolve) => setTimeout(resolve, 0)) - const { field, id: cellId } = params - apiRef.current.stopCellEditMode({ id: cellId, field }) - }} - /> - ) -} -export default observer(DataGrid) diff --git a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx index 369dd77a8..9cce23e41 100644 --- a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx +++ b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx @@ -16,17 +16,9 @@ import { import { FeatureAttributes } from './FeatureAttributes' import { featureContextMenuItems } from './featureContextMenuItems' import type { ContextMenuState } from './HybridGrid' +import { NumberCell } from './NumberCell' const useStyles = makeStyles()((theme) => ({ - levelIndicator: { - width: '1em', - height: '100%', - position: 'relative', - flex: 1, - marginLeft: '1em', - verticalAlign: 'top', - background: 'blue', - }, typeContent: { display: 'inline-block', width: '174px', @@ -215,38 +207,38 @@ export const Feature = observer(function Feature({ /> - { - const newValue = Number(e.target.textContent) - if (!Number.isNaN(newValue) && newValue !== feature.start) { + + handleFeatureStartChange( changeManager, feature, feature.start, - newValue, - ).catch(notifyError) + newStart, + ) } - }} - > - {feature.start} + /> - { - const newValue = Number(e.target.textContent) - if (!Number.isNaN(newValue) && newValue !== feature.end) { + + handleFeatureEndChange( changeManager, feature, - feature.end, - newValue, - ).catch(notifyError) + feature.start, + newEnd, + ) } - }} - > - {feature.end} + /> + + + {feature.strand === 1 ? '+' : feature.strand === -1 ? '-' : undefined} + {feature.phase} diff --git a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/HybridGrid.tsx b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/HybridGrid.tsx index 5635adfb3..6580c4237 100644 --- a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/HybridGrid.tsx +++ b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/HybridGrid.tsx @@ -81,6 +81,8 @@ const HybridGrid = observer(function HybridGrid({ Type Start End + Strand + Phase Attributes diff --git a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/NumberCell.tsx b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/NumberCell.tsx new file mode 100644 index 000000000..783f361b6 --- /dev/null +++ b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/NumberCell.tsx @@ -0,0 +1,77 @@ +import { observer } from 'mobx-react' +import React, { useEffect, useState } from 'react' +import { makeStyles } from 'tss-react/mui' + +const useStyles = makeStyles()((theme) => ({ + inputWrapper: { + position: 'relative', + }, + hiddenWidthSpan: { + padding: theme.spacing(0.5), + color: 'transparent', + }, + numberTextInput: { + border: 'none', + background: 'inherit', + font: 'inherit', + position: 'absolute', + width: '100%', + left: 0, + }, +})) + +interface NumberCellProps { + initialValue: number + notifyError(error: Error): void + onChangeCommitted(newValue: number): Promise +} + +export const NumberCell = observer(function NumberCell({ + initialValue, + notifyError, + onChangeCommitted, +}: NumberCellProps) { + const [value, setValue] = useState(initialValue) + const [blur, setBlur] = useState(false) + const [inputNode, setInputNode] = useState(null) + const { classes } = useStyles() + useEffect(() => { + if (blur) { + inputNode?.blur() + setBlur(false) + } + }, [blur, inputNode]) + function onChange(event: React.ChangeEvent) { + const newValue = Number(event.target.value) + if (!Number.isNaN(newValue)) { + setValue(newValue) + } + } + return ( + + + {value} + + { + if (event.key === 'Enter') { + inputNode?.blur() + } else if (event.key === 'Escape') { + setValue(initialValue) + setBlur(true) + } + }} + onBlur={() => { + if (value !== initialValue) { + onChangeCommitted(value).catch(notifyError) + } + }} + ref={(node) => setInputNode(node)} + /> + + ) +}) diff --git a/packages/jbrowse-plugin-apollo/src/components/ViewChangeLog.tsx b/packages/jbrowse-plugin-apollo/src/components/ViewChangeLog.tsx index b51f9876a..b3a6b03f8 100644 --- a/packages/jbrowse-plugin-apollo/src/components/ViewChangeLog.tsx +++ b/packages/jbrowse-plugin-apollo/src/components/ViewChangeLog.tsx @@ -149,7 +149,6 @@ export function ViewChangeLog({ handleClose, session }: ViewChangeLogProps) { return } const data = await response.json() - console.log({ data }) setDisplayGridData(data) } } diff --git a/packages/jbrowse-plugin-apollo/src/makeDisplayComponent.tsx b/packages/jbrowse-plugin-apollo/src/makeDisplayComponent.tsx index 5170eb0ee..cdd71e883 100644 --- a/packages/jbrowse-plugin-apollo/src/makeDisplayComponent.tsx +++ b/packages/jbrowse-plugin-apollo/src/makeDisplayComponent.tsx @@ -169,6 +169,12 @@ export const DisplayComponent = observer(function DisplayComponent({ () => scrollSelectedFeatureIntoView(model, canvasScrollContainerRef), [model, selectedFeature], ) + const headerStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + background: 'turquoise', + } return (
- +
+ +