jbrowse-plugin-protein3d 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/LaunchProteinView/components/FoldseekActionMenu.js +6 -16
  2. package/dist/LaunchProteinView/components/FoldseekResultsTable.js +14 -7
  3. package/dist/LaunchProteinView/components/ProteinViewActions.js +21 -38
  4. package/dist/LaunchProteinView/components/UniProtResultsTable.js +11 -9
  5. package/dist/LaunchProteinView/components/UserProvidedStructure.js +8 -16
  6. package/dist/LaunchProteinView/components/launchProteinAnnotationView.js +1 -1
  7. package/dist/LaunchProteinView/hooks/useSafeLaunch.d.ts +1 -1
  8. package/dist/LaunchProteinView/utils/launchHelpers.d.ts +6 -11
  9. package/dist/LaunchProteinView/utils/launchHelpers.js +6 -7
  10. package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +8 -2
  11. package/dist/LaunchProteinView/utils/launchViewUtils.js +31 -0
  12. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  13. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +3 -3
  14. package/dist/version.d.ts +1 -1
  15. package/dist/version.js +1 -1
  16. package/package.json +1 -1
  17. package/src/LaunchProteinView/components/FoldseekActionMenu.tsx +8 -25
  18. package/src/LaunchProteinView/components/FoldseekResultsTable.tsx +15 -6
  19. package/src/LaunchProteinView/components/ProteinViewActions.tsx +21 -45
  20. package/src/LaunchProteinView/components/UniProtResultsTable.tsx +12 -9
  21. package/src/LaunchProteinView/components/UserProvidedStructure.tsx +8 -15
  22. package/src/LaunchProteinView/components/launchProteinAnnotationView.ts +1 -1
  23. package/src/LaunchProteinView/hooks/useSafeLaunch.ts +1 -1
  24. package/src/LaunchProteinView/utils/launchHelpers.ts +6 -16
  25. package/src/LaunchProteinView/utils/launchViewUtils.ts +49 -5
  26. package/src/version.ts +1 -1
@@ -1,10 +1,9 @@
1
1
  import React, { useState } from 'react';
2
2
  import { ErrorMessage } from '@jbrowse/core/ui';
3
- import { isSessionWithAddTracks } from '@jbrowse/core/util';
4
3
  import { Button, Menu, MenuItem } from '@mui/material';
5
4
  import { useSafeLaunch } from '../hooks/useSafeLaunch';
6
5
  import { caCoordsToPdb, hasValidCaCoords } from '../utils/caCoordsToPdb';
7
- import { getConfidenceUrlFromTarget, getUniprotIdFromAlphaFoldTarget, hasMsaViewPlugin, launch1DProteinView, launch3DProteinView, launchMsaView, } from '../utils/launchViewUtils';
6
+ import { getConditionalProteinLaunches, getConfidenceUrlFromTarget, getUniprotIdFromAlphaFoldTarget, launch3DProteinView, } from '../utils/launchViewUtils';
8
7
  export default function FoldseekActionMenu({ hit, session, view, feature, selectedTranscript, userProvidedTranscriptSequence, onClose, }) {
9
8
  const [anchorEl, setAnchorEl] = useState(null);
10
9
  const open = Boolean(anchorEl);
@@ -29,28 +28,19 @@ export default function FoldseekActionMenu({ hit, session, view, feature, select
29
28
  userProvidedTranscriptSequence,
30
29
  });
31
30
  });
32
- const handleLaunchMSA = runLaunch(() => {
33
- launchMsaView(baseParams);
31
+ const { launch1D, launchMsa } = getConditionalProteinLaunches({
32
+ ...baseParams,
33
+ confidenceUrl: getConfidenceUrlFromTarget(hit.target),
34
34
  });
35
35
  const canLoad = !!hit.structureUrl || hasValidCaCoords(hit.tCa, hit.tSeq);
36
36
  if (!canLoad) {
37
37
  return React.createElement("span", null, "-");
38
38
  }
39
- // 1D launch needs an add-tracks session and a uniprotId; narrowing both here
40
- // gates the menu item and types its handler from a single condition.
41
- const addTracksSession = isSessionWithAddTracks(session) ? session : undefined;
42
39
  return (React.createElement(React.Fragment, null,
43
40
  launchError ? React.createElement(ErrorMessage, { error: launchError }) : null,
44
41
  React.createElement(Button, { size: "small", variant: "outlined", onClick: handleClick }, "Load"),
45
42
  React.createElement(Menu, { anchorEl: anchorEl, open: open, onClose: handleMenuClose },
46
43
  React.createElement(MenuItem, { onClick: handleLaunch3D }, "Launch 3D protein view"),
47
- addTracksSession && uniprotId ? (React.createElement(MenuItem, { onClick: runLaunch(() => launch1DProteinView({
48
- session: addTracksSession,
49
- view,
50
- feature,
51
- selectedTranscript,
52
- uniprotId,
53
- confidenceUrl: getConfidenceUrlFromTarget(hit.target),
54
- })) }, "Launch 1D protein annotation view")) : null,
55
- uniprotId && hasMsaViewPlugin() ? (React.createElement(MenuItem, { onClick: handleLaunchMSA }, "Launch MSA view (AlphaFoldDB a3m)")) : null)));
44
+ launch1D ? (React.createElement(MenuItem, { onClick: runLaunch(launch1D) }, "Launch 1D protein annotation view")) : null,
45
+ launchMsa ? (React.createElement(MenuItem, { onClick: runLaunch(launchMsa) }, "Launch MSA view (AlphaFoldDB a3m)")) : null)));
56
46
  }
@@ -3,7 +3,7 @@ import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow
3
3
  import { makeStyles } from 'tss-react/mui';
4
4
  import FoldseekActionMenu from './FoldseekActionMenu';
5
5
  import { getStructureUrlFromTarget } from '../utils/launchViewUtils';
6
- const useStyles = makeStyles()({
6
+ const useStyles = makeStyles()(theme => ({
7
7
  root: {
8
8
  display: 'flex',
9
9
  flexDirection: 'column',
@@ -14,13 +14,16 @@ const useStyles = makeStyles()({
14
14
  },
15
15
  headerCell: {
16
16
  fontWeight: 'bold',
17
- backgroundColor: '#f5f5f5',
17
+ backgroundColor: theme.palette.mode === 'dark'
18
+ ? theme.palette.grey[900]
19
+ : theme.palette.grey[100],
18
20
  },
19
21
  noResults: {
20
22
  padding: 16,
21
23
  textAlign: 'center',
22
24
  },
23
- });
25
+ }));
26
+ const MAX_HITS = 100;
24
27
  function flattenResults(results) {
25
28
  const hits = results.results.flatMap(dbResult => (dbResult.alignments ?? []).flat().map(alignment => ({
26
29
  ...alignment,
@@ -28,11 +31,12 @@ function flattenResults(results) {
28
31
  structureUrl: getStructureUrlFromTarget(alignment.target, dbResult.db),
29
32
  })));
30
33
  hits.sort((a, b) => (a.eval ?? Infinity) - (b.eval ?? Infinity));
31
- return hits.slice(0, 100);
34
+ return hits;
32
35
  }
33
36
  export default function FoldseekResultsTable({ results, session, view, feature, selectedTranscript, userProvidedTranscriptSequence, onClose, }) {
34
37
  const { classes } = useStyles();
35
- const flatHits = flattenResults(results);
38
+ const allHits = flattenResults(results);
39
+ const flatHits = allHits.slice(0, MAX_HITS);
36
40
  if (flatHits.length === 0) {
37
41
  return (React.createElement(Paper, { className: classes.noResults },
38
42
  React.createElement(Typography, null, "No similar structures found")));
@@ -40,8 +44,11 @@ export default function FoldseekResultsTable({ results, session, view, feature,
40
44
  return (React.createElement("div", { className: classes.root },
41
45
  React.createElement(Typography, { variant: "subtitle2" },
42
46
  "Found ",
43
- flatHits.length,
44
- " similar structures"),
47
+ allHits.length,
48
+ " similar structures",
49
+ allHits.length > flatHits.length
50
+ ? ` (showing top ${flatHits.length})`
51
+ : ''),
45
52
  React.createElement(TableContainer, { component: Paper, className: classes.tableContainer },
46
53
  React.createElement(Table, { size: "small", stickyHeader: true },
47
54
  React.createElement(TableHead, null,
@@ -1,6 +1,5 @@
1
1
  import React, { useState } from 'react';
2
2
  import { ErrorMessage } from '@jbrowse/core/ui';
3
- import { isSessionWithAddTracks } from '@jbrowse/core/util';
4
3
  import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
5
4
  import SettingsIcon from '@mui/icons-material/Settings';
6
5
  import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/material';
@@ -9,36 +8,31 @@ import LaunchSettingsDialog from './LaunchSettingsDialog';
9
8
  import SequenceMismatchNotice from './SequenceMismatchNotice';
10
9
  import { useSafeLaunch } from '../hooks/useSafeLaunch';
11
10
  import { getLaunchMissingReasons } from '../utils/launchHelpers';
12
- import { hasMsaViewPlugin, launch1DProteinView, launch3DProteinView, launch3DProteinViewWithMsa, launchMsaView, } from '../utils/launchViewUtils';
11
+ import { getConditionalProteinLaunches, launch3DProteinView, launch3DProteinViewWithMsa, } from '../utils/launchViewUtils';
13
12
  export default function ProteinViewActions({ handleClose, uniprotId, userSelectedProteinSequence, selectedTranscript, url, confidenceUrl, feature, view, session, alignmentAlgorithm, onAlignmentAlgorithmChange, sequencesMatch, isLoading, error, }) {
14
13
  const [dialogOpen, setDialogOpen] = useState(false);
15
14
  const [settingsOpen, setSettingsOpen] = useState(false);
16
- // Disable launch while loading — SWR's keepPreviousData would otherwise let
17
- // a user click Launch on stale results (wrong UniProt ID) during a refetch.
18
- const canLaunch = !isLoading &&
19
- !!uniprotId &&
20
- !!userSelectedProteinSequence &&
21
- !!selectedTranscript;
22
15
  const missingReasons = getLaunchMissingReasons({
23
16
  uniprotId,
24
17
  userSelectedProteinSequence,
25
18
  selectedTranscript,
26
- isLoading,
27
- error,
28
19
  });
20
+ // Disable launch while loading — SWR's keepPreviousData would otherwise let
21
+ // a user click Launch on stale results (wrong UniProt ID) during a refetch.
22
+ const canLaunch = !isLoading && missingReasons.length === 0;
23
+ // Suppress the derived reasons while loading or while a real upstream error
24
+ // is displayed above via <ErrorMessage> — a duplicate hint would mislead.
25
+ const showMissingReasons = !isLoading && !error && missingReasons.length > 0;
29
26
  const closeMenu = () => {
30
27
  setDialogOpen(false);
31
28
  };
32
29
  const { runLaunch, launchError } = useSafeLaunch(handleClose, closeMenu);
33
- const baseParams = {
30
+ const launch3DParams = {
34
31
  session,
35
32
  view,
36
33
  feature,
37
34
  selectedTranscript,
38
35
  uniprotId,
39
- };
40
- const launch3DParams = {
41
- ...baseParams,
42
36
  url,
43
37
  userProvidedTranscriptSequence: userSelectedProteinSequence?.seq,
44
38
  alignmentAlgorithm,
@@ -46,18 +40,14 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
46
40
  const handleLaunch3DView = runLaunch(() => {
47
41
  launch3DProteinView(launch3DParams);
48
42
  });
49
- const handleLaunchMsa = runLaunch(() => {
50
- launchMsaView(baseParams);
51
- });
52
- const handleLaunch3DWithMsa = runLaunch(() => {
53
- launch3DProteinViewWithMsa(launch3DParams);
43
+ const { launch1D, launchMsa } = getConditionalProteinLaunches({
44
+ session,
45
+ view,
46
+ feature,
47
+ selectedTranscript,
48
+ uniprotId,
49
+ confidenceUrl,
54
50
  });
55
- // The 1D annotation view needs an add-tracks session and a known uniprotId.
56
- // Narrowing here is the single source of truth: the option only exists when
57
- // both hold, and its handler is type-checked against those narrowed values —
58
- // so a 1D launch that can't work is unrepresentable rather than a silent
59
- // no-op.
60
- const addTracksSession = isSessionWithAddTracks(session) ? session : undefined;
61
51
  const launchOptions = [
62
52
  {
63
53
  key: '3d',
@@ -65,36 +55,29 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
65
55
  description: 'View protein structure with genome-to-structure coordinate mapping',
66
56
  onClick: handleLaunch3DView,
67
57
  },
68
- ...(addTracksSession && uniprotId
58
+ ...(launch1D
69
59
  ? [
70
60
  {
71
61
  key: '1d',
72
62
  title: 'Launch 1D protein annotation view',
73
63
  description: 'View protein features and annotations as a linear track',
74
- onClick: runLaunch(() => launch1DProteinView({
75
- session: addTracksSession,
76
- view,
77
- feature,
78
- selectedTranscript,
79
- uniprotId,
80
- confidenceUrl,
81
- })),
64
+ onClick: runLaunch(launch1D),
82
65
  },
83
66
  ]
84
67
  : []),
85
- ...(hasMsaViewPlugin()
68
+ ...(launchMsa
86
69
  ? [
87
70
  {
88
71
  key: 'msa',
89
72
  title: 'Launch MSA view',
90
73
  description: 'View AlphaFold a3m multiple sequence alignment',
91
- onClick: handleLaunchMsa,
74
+ onClick: runLaunch(launchMsa),
92
75
  },
93
76
  {
94
77
  key: '3d-msa',
95
78
  title: 'Launch 3D structure + MSA view',
96
79
  description: 'Launch both views with AlphaFold a3m MSA',
97
- onClick: handleLaunch3DWithMsa,
80
+ onClick: runLaunch(() => launch3DProteinViewWithMsa(launch3DParams)),
98
81
  },
99
82
  ]
100
83
  : []),
@@ -110,7 +93,7 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
110
93
  React.createElement(Button, { variant: "contained", color: "secondary", size: "small", onClick: () => {
111
94
  handleClose();
112
95
  } }, "Cancel"),
113
- !canLaunch && missingReasons.length > 0 ? (React.createElement(Typography, { variant: "body2", color: "error", sx: { mr: 2 } }, missingReasons.join('. '))) : null,
96
+ showMissingReasons ? (React.createElement(Typography, { variant: "body2", color: "error", sx: { mr: 2 } }, missingReasons.join('. '))) : null,
114
97
  React.createElement(ButtonGroup, { variant: "contained", color: "primary", size: "small" },
115
98
  React.createElement(Button, { disabled: !canLaunch, onClick: handleLaunch3DView }, "Launch"),
116
99
  React.createElement(Button, { disabled: !canLaunch, onClick: () => {
@@ -2,36 +2,38 @@ import React from 'react';
2
2
  import { Chip, Paper, Radio, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, } from '@mui/material';
3
3
  import { makeStyles } from 'tss-react/mui';
4
4
  import ExternalLink from '../../components/ExternalLink';
5
- const useStyles = makeStyles()({
5
+ const useStyles = makeStyles()(theme => ({
6
6
  tableContainer: {
7
7
  maxHeight: 200,
8
8
  },
9
9
  headerCell: {
10
10
  fontWeight: 'bold',
11
- backgroundColor: '#f5f5f5',
11
+ backgroundColor: theme.palette.mode === 'dark'
12
+ ? theme.palette.grey[900]
13
+ : theme.palette.grey[100],
12
14
  },
13
15
  selectedRow: {
14
- backgroundColor: '#e3f2fd',
16
+ backgroundColor: theme.palette.action.selected,
15
17
  },
16
18
  clickableRow: {
17
19
  cursor: 'pointer',
18
20
  '&:hover': {
19
- backgroundColor: '#f5f5f5',
21
+ backgroundColor: theme.palette.action.hover,
20
22
  },
21
23
  },
22
24
  reviewedChip: {
23
- backgroundColor: '#4caf50',
24
- color: 'white',
25
+ backgroundColor: theme.palette.success.main,
26
+ color: theme.palette.success.contrastText,
25
27
  fontSize: '0.7rem',
26
28
  height: 20,
27
29
  },
28
30
  unreviewedChip: {
29
- backgroundColor: '#ff9800',
30
- color: 'white',
31
+ backgroundColor: theme.palette.warning.main,
32
+ color: theme.palette.warning.contrastText,
31
33
  fontSize: '0.7rem',
32
34
  height: 20,
33
35
  },
34
- });
36
+ }));
35
37
  export default function UniProtResultsTable({ entries, selectedAccession, onSelect, }) {
36
38
  const { classes, cx } = useStyles();
37
39
  return entries.length === 0 ? null : (React.createElement("div", null,
@@ -8,6 +8,7 @@ import SequenceMismatchNotice from './SequenceMismatchNotice';
8
8
  import StructureSourcePicker from './StructureSourcePicker';
9
9
  import TranscriptSelector from './TranscriptSelector';
10
10
  import ExternalLink from '../../components/ExternalLink';
11
+ import { useSafeLaunch } from '../hooks/useSafeLaunch';
11
12
  import useStructureFileSequence from '../hooks/useStructureFileSequence';
12
13
  import useTranscriptIsoformSelection from '../hooks/useTranscriptIsoformSelection';
13
14
  import { launch3DProteinView } from '../utils/launchViewUtils';
@@ -33,43 +34,34 @@ const UserProvidedStructure = observer(function UserProvidedStructure({ feature,
33
34
  const [file, setFile] = useState();
34
35
  const [pdbId, setPdbId] = useState('');
35
36
  const [choice, setChoice] = useState('file');
36
- const [submitError, setSubmitError] = useState();
37
37
  const [structureURL, setStructureURL] = useState('');
38
+ const { runLaunch, launchError } = useSafeLaunch(handleClose);
38
39
  const activeFile = choice === 'file' ? file : undefined;
39
40
  const activeURL = choice === 'file' ? '' : structureURL;
40
41
  const { sequences: structureSequences, error: fileError } = useStructureFileSequence({ file: activeFile, url: activeURL });
41
42
  const structureName = activeFile?.name ?? activeURL.slice(activeURL.lastIndexOf('/') + 1);
42
43
  const structureSequence = structureSequences?.[0];
43
44
  const { transcripts: options, isoformSequences, selectedTranscriptId: userSelection, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: protein, error: isoformError, } = useTranscriptIsoformSelection({ feature, view, structureSequence });
44
- const error = isoformError ?? submitError ?? fileError;
45
+ const error = isoformError ?? launchError ?? fileError;
45
46
  const canLaunch = !!(activeURL || activeFile) && !!protein && !!selectedTranscript;
46
47
  const sequencesDiffer = !!protein?.seq &&
47
48
  !!structureSequence &&
48
49
  stripStopCodon(protein.seq) !== structureSequence;
49
- const handleLaunch = async () => {
50
- if (!protein || !selectedTranscript) {
51
- return;
52
- }
53
- try {
50
+ const handleLaunch = runLaunch(async () => {
51
+ if (protein && selectedTranscript) {
54
52
  const structureData = activeFile ? await activeFile.text() : undefined;
55
- const url = activeURL ? activeURL : undefined;
56
53
  launch3DProteinView({
57
54
  session,
58
55
  view,
59
56
  feature,
60
57
  selectedTranscript,
61
- url,
58
+ url: activeURL ? activeURL : undefined,
62
59
  data: structureData,
63
60
  userProvidedTranscriptSequence: protein.seq,
64
61
  alignmentAlgorithm,
65
62
  });
66
- handleClose();
67
- }
68
- catch (e) {
69
- console.error(e);
70
- setSubmitError(e);
71
63
  }
72
- };
64
+ });
73
65
  return (React.createElement(React.Fragment, null,
74
66
  React.createElement(DialogContent, { className: classes.dialogContent },
75
67
  error ? React.createElement(ErrorMessage, { error: error }) : null,
@@ -84,7 +76,7 @@ const UserProvidedStructure = observer(function UserProvidedStructure({ feature,
84
76
  handleClose();
85
77
  } }, "Cancel"),
86
78
  React.createElement(Button, { variant: "contained", color: "primary", disabled: !canLaunch, onClick: () => {
87
- void handleLaunch();
79
+ handleLaunch();
88
80
  } }, "Launch 3-D protein structure view"))));
89
81
  });
90
82
  export default UserProvidedStructure;
@@ -11,7 +11,7 @@ export async function launchProteinAnnotationView({ session, feature, selectedTr
11
11
  });
12
12
  const view = session.addView('LinearGenomeView', {
13
13
  type: 'LinearGenomeView',
14
- displayName: formatViewName('Protein view', feature, selectedTranscript, uniprotId),
14
+ displayName: formatViewName('Protein annotations', feature, selectedTranscript, uniprotId),
15
15
  });
16
16
  // Register for linked highlighting between 1D and 3D views
17
17
  if (connectedViewId && selectedTranscript) {
@@ -4,6 +4,6 @@
4
4
  * runs the launch via safeLaunch, and surfaces failures inline.
5
5
  */
6
6
  export declare function useSafeLaunch(onSuccess: () => void, onBeforeLaunch?: () => void): {
7
- runLaunch: (fn: () => void | Promise<void>) => () => void;
7
+ runLaunch: (fn: () => unknown) => () => void;
8
8
  launchError: unknown;
9
9
  };
@@ -3,24 +3,19 @@
3
3
  * Used to wrap `session.addView(...)` calls so MST validation errors don't
4
4
  * fall silently into the React error boundary.
5
5
  */
6
- export declare function safeLaunch(fn: () => void | Promise<void>, onSuccess?: () => void, onError?: (e: unknown) => void): Promise<void>;
6
+ export declare function safeLaunch(fn: () => unknown, onSuccess?: () => void, onError?: (e: unknown) => void): Promise<void>;
7
7
  interface LaunchRequirements {
8
8
  uniprotId?: string;
9
9
  userSelectedProteinSequence?: {
10
10
  seq: string;
11
11
  };
12
12
  selectedTranscript?: unknown;
13
- isLoading?: boolean;
14
- /**
15
- * If a real error already surfaced (e.g. from UniProt lookup), suppress the
16
- * derived "No UniProt ID found" reason — the underlying error is already
17
- * shown via <ErrorMessage> and the duplicate hint is misleading.
18
- */
19
- error?: unknown;
20
13
  }
21
14
  /**
22
- * Compute user-facing reasons the Launch button is disabled. Suppressed
23
- * while loading or while a real upstream error is being displayed.
15
+ * The requirements a launch needs, as user-facing reasons for any that are
16
+ * unmet. An empty array means the launch can proceed. Callers decide whether to
17
+ * surface these (e.g. suppressed while loading or while a real upstream error
18
+ * is already shown via <ErrorMessage>, where a duplicate hint would mislead).
24
19
  */
25
- export declare function getLaunchMissingReasons({ uniprotId, userSelectedProteinSequence, selectedTranscript, isLoading, error, }: LaunchRequirements): string[];
20
+ export declare function getLaunchMissingReasons({ uniprotId, userSelectedProteinSequence, selectedTranscript, }: LaunchRequirements): string[];
26
21
  export {};
@@ -14,16 +14,15 @@ export async function safeLaunch(fn, onSuccess, onError) {
14
14
  }
15
15
  }
16
16
  /**
17
- * Compute user-facing reasons the Launch button is disabled. Suppressed
18
- * while loading or while a real upstream error is being displayed.
17
+ * The requirements a launch needs, as user-facing reasons for any that are
18
+ * unmet. An empty array means the launch can proceed. Callers decide whether to
19
+ * surface these (e.g. suppressed while loading or while a real upstream error
20
+ * is already shown via <ErrorMessage>, where a duplicate hint would mislead).
19
21
  */
20
- export function getLaunchMissingReasons({ uniprotId, userSelectedProteinSequence, selectedTranscript, isLoading, error, }) {
21
- if (isLoading || error) {
22
- return [];
23
- }
22
+ export function getLaunchMissingReasons({ uniprotId, userSelectedProteinSequence, selectedTranscript, }) {
24
23
  return [
25
24
  !uniprotId && 'No UniProt ID found',
26
- !userSelectedProteinSequence &&
25
+ !userSelectedProteinSequence?.seq &&
27
26
  'Could not compute protein sequence (feature may be missing CDS subfeatures)',
28
27
  !selectedTranscript && 'No transcript selected',
29
28
  ].filter((s) => typeof s === 'string');
@@ -1,10 +1,10 @@
1
+ import type { AbstractSessionModel, Feature, SessionWithAddTracks } from '@jbrowse/core/util';
2
+ import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
1
3
  declare global {
2
4
  interface Window {
3
5
  JBrowsePluginMsaView?: unknown;
4
6
  }
5
7
  }
6
- import type { AbstractSessionModel, Feature, SessionWithAddTracks } from '@jbrowse/core/util';
7
- import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
8
8
  export declare const ALPHAFOLD_VERSION = "v6";
9
9
  export declare function getAlphaFoldStructureUrl(uniprotId: string, version?: string): string;
10
10
  export declare function getAlphaFoldConfidenceUrl(uniprotId: string, version?: string): string;
@@ -41,5 +41,11 @@ export declare function launchMsaView({ session, view, feature, selectedTranscri
41
41
  displayName?: string;
42
42
  }): import("@jbrowse/core/util").AbstractViewModel | undefined;
43
43
  export declare function hasMsaViewPlugin(): boolean;
44
+ export declare function getConditionalProteinLaunches({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }: LaunchViewParams & {
45
+ confidenceUrl?: string;
46
+ }): {
47
+ launch1D: (() => Promise<void>) | undefined;
48
+ launchMsa: (() => import("@jbrowse/core/util").AbstractViewModel | undefined) | undefined;
49
+ };
44
50
  export declare function launch3DProteinViewWithMsa(params: LaunchViewParams & Launch3DExtraParams): import("@jbrowse/core/util").AbstractViewModel | undefined;
45
51
  export {};
@@ -1,3 +1,4 @@
1
+ import { isSessionWithAddTracks } from '@jbrowse/core/util';
1
2
  import { maybeLaunchSideBySide } from './sideBySide';
2
3
  import { getGeneDisplayName, getTranscriptDisplayName } from './util';
3
4
  import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView';
@@ -120,6 +121,36 @@ export function launchMsaView({ session, view, feature, selectedTranscript, unip
120
121
  export function hasMsaViewPlugin() {
121
122
  return window.JBrowsePluginMsaView !== undefined;
122
123
  }
124
+ // The 1D-annotation and MSA launches share identical availability rules across
125
+ // the AlphaFold and Foldseek launch menus: the 1D view needs an add-tracks
126
+ // session and a uniprotId, the MSA view needs the msaview plugin and a
127
+ // uniprotId. Returning each as a ready-to-run thunk (or undefined when
128
+ // unavailable) is the single source of truth — an unavailable action is
129
+ // unrepresentable rather than a menu item that silently no-ops.
130
+ export function getConditionalProteinLaunches({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }) {
131
+ const addTracksSession = isSessionWithAddTracks(session) ? session : undefined;
132
+ return {
133
+ launch1D: addTracksSession && uniprotId
134
+ ? () => launch1DProteinView({
135
+ session: addTracksSession,
136
+ view,
137
+ feature,
138
+ selectedTranscript,
139
+ uniprotId,
140
+ confidenceUrl,
141
+ })
142
+ : undefined,
143
+ launchMsa: uniprotId && hasMsaViewPlugin()
144
+ ? () => launchMsaView({
145
+ session,
146
+ view,
147
+ feature,
148
+ selectedTranscript,
149
+ uniprotId,
150
+ })
151
+ : undefined,
152
+ };
153
+ }
123
154
  export function launch3DProteinViewWithMsa(params) {
124
155
  const { uniprotId } = params;
125
156
  if (!uniprotId) {