@apollo-annotation/jbrowse-plugin-apollo 0.1.21 → 0.2.1

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 (57) hide show
  1. package/dist/index.esm.js +431 -570
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +439 -578
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +11064 -1091
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +4 -5
  12. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +4 -2
  13. package/src/ApolloInternetAccount/configSchema.ts +1 -1
  14. package/src/ApolloInternetAccount/model.ts +5 -10
  15. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +1 -1
  16. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +4 -5
  17. package/src/BackendDrivers/DesktopFileDriver.ts +3 -2
  18. package/src/FeatureDetailsWidget/Attributes.tsx +1 -6
  19. package/src/FeatureDetailsWidget/NumberTextField.tsx +1 -0
  20. package/src/FeatureDetailsWidget/StringTextField.tsx +1 -0
  21. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +131 -382
  22. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +209 -284
  23. package/src/FeatureDetailsWidget/model.ts +4 -4
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -0
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +25 -3
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +95 -32
  27. package/src/LinearApolloDisplay/stateModel/base.ts +5 -3
  28. package/src/LinearApolloDisplay/stateModel/index.ts +1 -1
  29. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +1 -1
  30. package/src/LinearApolloDisplay/stateModel/rendering.ts +1 -1
  31. package/src/OntologyManager/OntologyStore/fulltext.ts +5 -2
  32. package/src/OntologyManager/OntologyStore/index.ts +25 -22
  33. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +8 -3
  34. package/src/OntologyManager/index.ts +31 -8
  35. package/src/SixFrameFeatureDisplay/stateModel.ts +1 -1
  36. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +1 -0
  37. package/src/TabularEditor/HybridGrid/NumberCell.tsx +1 -0
  38. package/src/TabularEditor/model.ts +1 -1
  39. package/src/components/AddChildFeature.tsx +1 -0
  40. package/src/components/AddFeature.tsx +1 -1
  41. package/src/components/AddRefSeqAliases.tsx +1 -0
  42. package/src/components/CopyFeature.tsx +1 -0
  43. package/src/components/DeleteAssembly.tsx +1 -0
  44. package/src/components/DeleteFeature.tsx +1 -0
  45. package/src/components/LogOut.tsx +2 -1
  46. package/src/components/ManageChecks.tsx +1 -1
  47. package/src/components/ManageUsers.tsx +2 -1
  48. package/src/components/OntologyTermAutocomplete.tsx +7 -9
  49. package/src/components/OntologyTermMultiSelect.tsx +2 -1
  50. package/src/components/OpenLocalFile.tsx +3 -1
  51. package/src/components/ViewChangeLog.tsx +1 -0
  52. package/src/components/ViewCheckResults.tsx +1 -0
  53. package/src/config.ts +5 -0
  54. package/src/extensions/annotationFromPileup.ts +1 -1
  55. package/src/makeDisplayComponent.tsx +28 -7
  56. package/src/session/ClientDataStore.ts +1 -1
  57. package/src/session/session.ts +2 -1
package/dist/index.esm.js CHANGED
@@ -3,7 +3,7 @@ import { gff3ToAnnotationFeature, AddAssemblyFromExternalChange, AddAssemblyAndF
3
3
  import { ConfigurationSchema, readConfObject, getConf, ConfigurationReference } from '@jbrowse/core/configuration';
4
4
  import { BaseInternetAccountConfig, InternetAccount, RendererType, TextSearchAdapterType, BaseDisplay, WidgetType, createBaseTrackConfig, TrackType, createBaseTrackModel, InternetAccountType, DisplayType } from '@jbrowse/core/pluggableElementTypes';
5
5
  import Plugin from '@jbrowse/core/Plugin';
6
- import { checkAbortSignal, isUriLocation, isAbortException, isElectron, isAbstractMenuManager, getSession, getContainingView, revcom, isSessionModelWithWidgets, doesIntersect2, defaultCodonTable, getFrame, intersection2, reverse, defaultStarts, defaultStops } from '@jbrowse/core/util';
6
+ import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getSession, getContainingView, getFrame, revcom, isSessionModelWithWidgets, doesIntersect2, defaultCodonTable, intersection2, reverse, defaultStarts, defaultStops } from '@jbrowse/core/util';
7
7
  import AddIcon from '@mui/icons-material/Add';
8
8
  import { autorun, toJS, observable } from 'mobx';
9
9
  import { getSnapshot, getParent, getRoot, types, addDisposer, flow, isAlive, resolveIdentifier, getParentOfType, applySnapshot } from 'mobx-state-tree';
@@ -21,10 +21,11 @@ import CloseIcon from '@mui/icons-material/Close';
21
21
  import { observer } from 'mobx-react';
22
22
  import { makeStyles } from 'tss-react/mui';
23
23
  import { LocalPathLocation, UriLocation, BlobLocation, ElementId } from '@jbrowse/core/util/types/mst';
24
+ import { openDB, deleteDB } from 'idb/with-async-ittr';
25
+ import { checkAbortSignal, isAbortException } from '@jbrowse/core/util/aborting';
24
26
  import jsonpath from 'jsonpath';
25
27
  import { openLocation } from '@jbrowse/core/util/io';
26
28
  import equal from 'fast-deep-equal/es6';
27
- import { openDB } from 'idb/with-async-ittr';
28
29
  import { saveAs } from 'file-saver';
29
30
  import Checkbox$1 from '@mui/material/Checkbox';
30
31
  import FormControlLabel$1 from '@mui/material/FormControlLabel';
@@ -49,7 +50,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
49
50
  import ErrorIcon from '@mui/icons-material/Error';
50
51
  import SaveIcon from '@mui/icons-material/Save';
51
52
 
52
- var version = "0.1.21";
53
+ var version = "0.2.1";
53
54
 
54
55
  const ApolloConfigSchema = ConfigurationSchema('ApolloInternetAccount', {
55
56
  baseURL: {
@@ -685,7 +686,7 @@ function elaborateMatch(textIndexPaths, term, queryWordIndexes, queryWords, pref
685
686
  const sortedWordIndexes = [...queryWordIndexes].sort();
686
687
  const matchedQueryWords = sortedWordIndexes.map((i) => queryWords[i]);
687
688
  const queryWordRegexps = matchedQueryWords.map((queryWord) => {
688
- const escaped = queryWord.replaceAll(/[$()*+./?[\\\]^{|}-]/g, '\\$&');
689
+ const escaped = queryWord.replaceAll(/[$()*+./?[\\\]^{|}-]/g, String.raw `\$&`);
689
690
  return new RegExp(`\\b${escaped}`, 'gi');
690
691
  });
691
692
  // const needle = matchedQueryWords.join(' ')
@@ -832,7 +833,13 @@ async function loadOboGraphJson(db) {
832
833
  // TODO: using file streaming along with an event-based json parser
833
834
  // instead of JSON.parse and .readFile could probably make this faster
834
835
  // and less memory intensive
835
- const oboGraph = JSON.parse(await openLocation(this.sourceLocation).readFile('utf8'));
836
+ let oboGraph;
837
+ try {
838
+ oboGraph = JSON.parse(await openLocation(this.sourceLocation).readFile('utf8'));
839
+ }
840
+ catch {
841
+ throw new Error('Error in loading ontology');
842
+ }
836
843
  const parseTime = Date.now();
837
844
  const [graph, ...additionalGraphs] = oboGraph.graphs ?? [];
838
845
  if (!graph) {
@@ -911,12 +918,6 @@ async function isDatabaseCurrent(db) {
911
918
  }
912
919
 
913
920
  /* eslint-disable @typescript-eslint/only-throw-error */
914
- /**
915
- * @deprecated use the one from jbrowse core when it is published
916
- **/
917
- function isLocalPathLocation(location) {
918
- return (typeof location === 'object' && location !== null && 'localPath' in location);
919
- }
920
921
  async function arrayFromAsync(iter) {
921
922
  const a = [];
922
923
  for await (const i of iter) {
@@ -991,14 +992,21 @@ class OntologyStore {
991
992
  if (await this.isDatabaseCurrent(db)) {
992
993
  return db;
993
994
  }
994
- const { sourceLocation, sourceType } = this;
995
- if (sourceType === 'obo-graph-json') {
996
- await this.loadOboGraphJson(db);
995
+ try {
996
+ const { sourceLocation, sourceType } = this;
997
+ if (sourceType === 'obo-graph-json') {
998
+ await this.loadOboGraphJson(db);
999
+ }
1000
+ else {
1001
+ throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
1002
+ }
1003
+ return db;
997
1004
  }
998
- else {
999
- throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
1005
+ catch (error) {
1006
+ db.close();
1007
+ await deleteDB(this.dbName);
1008
+ throw error;
1000
1009
  }
1001
- return db;
1002
1010
  }
1003
1011
  async termCount(tx) {
1004
1012
  const db = await this.db;
@@ -1176,7 +1184,7 @@ class OntologyStore {
1176
1184
  }
1177
1185
  // fetch the full nodes and filter out deprecated ones
1178
1186
  const terms = [];
1179
- for await (const termId of termIds) {
1187
+ for (const termId of termIds) {
1180
1188
  const node = await myTx.objectStore('nodes').get(termId);
1181
1189
  if (node && isOntologyClass(node) && !isDeprecated(node)) {
1182
1190
  terms.push(node);
@@ -1234,15 +1242,22 @@ const OntologyManagerType = types
1234
1242
  'SO:': 'http://purl.obolibrary.org/obo/SO_',
1235
1243
  }),
1236
1244
  })
1245
+ .views((self) => ({
1246
+ get featureTypeOntologyName() {
1247
+ const jbConfig = getRoot(self).jbrowse
1248
+ .configuration;
1249
+ const pluginConfiguration = jbConfig.ApolloPlugin;
1250
+ const featureTypeOntologyName = readConfObject(pluginConfiguration, 'featureTypeOntologyName');
1251
+ return featureTypeOntologyName;
1252
+ },
1253
+ }))
1237
1254
  .views((self) => ({
1238
1255
  /**
1239
1256
  * gets the OntologyRecord for the ontology we should be
1240
1257
  * using for feature types (e.g. SO or maybe biotypes)
1241
1258
  **/
1242
1259
  get featureTypeOntology() {
1243
- // TODO: change this to read some configuration for which feature type ontology
1244
- // we should be using. currently hardcoded to use SO.
1245
- return this.findOntology('Sequence Ontology');
1260
+ return this.findOntology(self.featureTypeOntologyName);
1246
1261
  },
1247
1262
  findOntology(name, version) {
1248
1263
  return self.ontologies.find((record) => {
@@ -1354,7 +1369,6 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1354
1369
  // effect for clearing choices when not open
1355
1370
  useEffect(() => {
1356
1371
  if (!open) {
1357
- // eslint-disable-next-line unicorn/no-useless-undefined
1358
1372
  setTermChoices(undefined);
1359
1373
  }
1360
1374
  }, [open]);
@@ -1389,7 +1403,7 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1389
1403
  }
1390
1404
  }, (error) => {
1391
1405
  if (!signal.aborted && !isAbortException(error)) {
1392
- session.notify(error.message, 'error');
1406
+ session.notify(error instanceof Error ? error.message : String(error), 'error');
1393
1407
  }
1394
1408
  });
1395
1409
  }
@@ -1408,7 +1422,6 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1408
1422
  return;
1409
1423
  }
1410
1424
  if (typeof newValue === 'string') {
1411
- // eslint-disable-next-line unicorn/no-useless-undefined
1412
1425
  setCurrentOntologyTerm(undefined);
1413
1426
  onChange(valueString, newValue);
1414
1427
  }
@@ -1443,7 +1456,7 @@ async function getCurrentTerm(ontologyStore, currentTermLabel, filterTerms, _sig
1443
1456
  }
1444
1457
  // TODO: support prefixed IDs as ontology terms here (e.g. SO:001234)
1445
1458
  const terms = await ontologyStore.getTermsWithLabelOrSynonym(currentTermLabel, { includeSubclasses: false });
1446
- const term = terms.find(filterTerms ?? (() => true));
1459
+ const term = terms.find((term) => (filterTerms ?? (() => true))(term));
1447
1460
  if (!term) {
1448
1461
  throw new Error(`not a valid ${ontologyStore.ontologyName} term`);
1449
1462
  }
@@ -1525,7 +1538,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
1525
1538
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
1526
1539
  }
1527
1540
 
1528
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
1541
+ /* eslint-disable @typescript-eslint/unbound-method */
1529
1542
  function AddFeature({ changeManager, handleClose, region, session, }) {
1530
1543
  const { notify } = session;
1531
1544
  const [end, setEnd] = useState(String(region.end));
@@ -1585,7 +1598,6 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
1585
1598
  break;
1586
1599
  }
1587
1600
  default: {
1588
- // eslint-disable-next-line unicorn/no-useless-undefined
1589
1601
  setStrand(undefined);
1590
1602
  }
1591
1603
  }
@@ -1800,7 +1812,7 @@ function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, so
1800
1812
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
1801
1813
  }
1802
1814
 
1803
- /* eslint-disable @typescript-eslint/no-misused-promises */
1815
+ /* eslint-disable @typescript-eslint/unbound-method */
1804
1816
  function DeleteAssembly({ changeManager, handleClose, session, }) {
1805
1817
  const { internetAccounts } = getRoot(session);
1806
1818
  const [selectedAssembly, setSelectedAssembly] = useState();
@@ -2183,6 +2195,7 @@ function ImportFeatures({ changeManager, handleClose, session, }) {
2183
2195
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
2184
2196
  }
2185
2197
 
2198
+ /* eslint-disable @typescript-eslint/unbound-method */
2186
2199
  function LogOut({ handleClose, session }) {
2187
2200
  const { internetAccounts } = getRoot(session);
2188
2201
  const [errorMessage, setErrorMessage] = useState('');
@@ -2202,7 +2215,7 @@ function LogOut({ handleClose, session }) {
2202
2215
  event.preventDefault();
2203
2216
  setErrorMessage('');
2204
2217
  selectedInternetAccount.removeToken();
2205
- window.location.reload();
2218
+ globalThis.location.reload();
2206
2219
  }
2207
2220
  return (React__default.createElement(Dialog, { open: true, title: "Log out", handleClose: handleClose, maxWidth: false, "data-testid": "log-out" },
2208
2221
  React__default.createElement("form", { onSubmit: onSubmit },
@@ -2323,7 +2336,7 @@ function ManageChecks({ handleClose, session }) {
2323
2336
  }
2324
2337
  else {
2325
2338
  const index = checks.indexOf(_id, 0);
2326
- if (index > -1) {
2339
+ if (index !== -1) {
2327
2340
  checks.splice(index, 1);
2328
2341
  }
2329
2342
  setSelectedChecks(checks);
@@ -2364,7 +2377,7 @@ function ManageChecks({ handleClose, session }) {
2364
2377
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
2365
2378
  }
2366
2379
 
2367
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
2380
+ /* eslint-disable @typescript-eslint/unbound-method */
2368
2381
  function ManageUsers({ changeManager, handleClose, session, }) {
2369
2382
  const { internetAccounts } = getRoot(session);
2370
2383
  const apolloInternetAccounts = internetAccounts.filter((ia) => ia.type === 'ApolloInternetAccount' && ia.role?.includes('admin'));
@@ -2448,7 +2461,7 @@ function ManageUsers({ changeManager, handleClose, session, }) {
2448
2461
  type: 'actions',
2449
2462
  getActions: (params) => [
2450
2463
  React__default.createElement(GridActionsCellItem, { key: `delete-${params.id}`, icon: React__default.createElement(DeleteIcon, null), onClick: async () => {
2451
- if (window.confirm('Delete this user?')) {
2464
+ if (globalThis.confirm('Delete this user?')) {
2452
2465
  await deleteUser(params.id);
2453
2466
  }
2454
2467
  }, disabled: isCurrentUser(params.id), label: "Delete" }),
@@ -2488,7 +2501,7 @@ function ManageUsers({ changeManager, handleClose, session, }) {
2488
2501
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
2489
2502
  }
2490
2503
 
2491
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
2504
+ /* eslint-disable @typescript-eslint/unbound-method */
2492
2505
  // interface TermAutocompleteResult extends TermValue {
2493
2506
  // label: string[]
2494
2507
  // match: string
@@ -2900,7 +2913,7 @@ function ModifyFeatureAttribute({ changeManager, handleClose, session, sourceAss
2900
2913
  React__default.createElement(Button, { variant: "outlined", type: "submit", disabled: showAddNewForm, onClick: handleClose }, "Cancel")))));
2901
2914
  }
2902
2915
 
2903
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2916
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2904
2917
  function OpenLocalFile({ handleClose, session }) {
2905
2918
  const { apolloDataStore } = session;
2906
2919
  const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
@@ -2996,7 +3009,7 @@ function OpenLocalFile({ handleClose, session }) {
2996
3009
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
2997
3010
  }
2998
3011
 
2999
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
3012
+ /* eslint-disable @typescript-eslint/unbound-method */
3000
3013
  const useStyles$d = makeStyles()((theme) => ({
3001
3014
  changeTextarea: {
3002
3015
  fontFamily: 'monospace',
@@ -3520,7 +3533,9 @@ const AuthTypeSelector = ({ baseURL, handleClose, name, }) => {
3520
3533
  setLoginTypes(data);
3521
3534
  }
3522
3535
  getAuthTypes().catch((error) => {
3523
- isAbortException(error) ? '' : setErrorMessage(String(error));
3536
+ if (!isAbortException(error)) {
3537
+ setErrorMessage(String(error));
3538
+ }
3524
3539
  });
3525
3540
  return () => {
3526
3541
  controller.abort();
@@ -3643,7 +3658,7 @@ const stateModelFactory$2 = (configSchema) => {
3643
3658
  async openAuthWindow(type, resolve, reject) {
3644
3659
  const redirectUri = isElectron
3645
3660
  ? 'http://localhost/auth'
3646
- : window.location.origin + window.location.pathname;
3661
+ : globalThis.location.origin + globalThis.location.pathname;
3647
3662
  const url = new URL('auth/login', self.baseURL);
3648
3663
  const params = new URLSearchParams({
3649
3664
  type,
@@ -3652,7 +3667,7 @@ const stateModelFactory$2 = (configSchema) => {
3652
3667
  url.search = params.toString();
3653
3668
  const eventName = `JBrowseAuthWindow-${self.internetAccountId}`;
3654
3669
  if (isElectron) {
3655
- const { ipcRenderer } = window.require('electron');
3670
+ const { ipcRenderer } = globalThis.require('electron');
3656
3671
  const redirectUriFromElectron = await ipcRenderer.invoke('openAuthWindow', {
3657
3672
  internetAccountId: self.internetAccountId,
3658
3673
  data: { redirect_uri: redirectUri },
@@ -3826,14 +3841,9 @@ const stateModelFactory$2 = (configSchema) => {
3826
3841
  }
3827
3842
  });
3828
3843
  socket.on('REQUEST_INFORMATION', (message) => {
3829
- const { channel, reqType, userSessionId } = message;
3844
+ const { channel, userSessionId } = message;
3830
3845
  if (channel === 'REQUEST_INFORMATION' && userSessionId !== token) {
3831
- switch (reqType) {
3832
- case 'CURRENT_LOCATION': {
3833
- session.broadcastLocations();
3834
- break;
3835
- }
3836
- }
3846
+ session.broadcastLocations();
3837
3847
  }
3838
3848
  });
3839
3849
  },
@@ -4505,7 +4515,6 @@ function ApolloRendering(props) {
4505
4515
  }
4506
4516
  await changeManager.submit(change);
4507
4517
  }
4508
- // eslint-disable-next-line unicorn/no-useless-undefined
4509
4518
  setDragging(undefined);
4510
4519
  setMovedDuringLastMouseDown(false);
4511
4520
  }
@@ -4518,7 +4527,6 @@ function ApolloRendering(props) {
4518
4527
  React__default.createElement(Menu, { open: Boolean(contextMenuFeature), anchorReference: "anchorPosition", anchorPosition: contextCoord
4519
4528
  ? { left: contextCoord[0], top: contextCoord[1] }
4520
4529
  : undefined, "data-testid": "base_linear_display_context_menu", onClose: () => {
4521
- // eslint-disable-next-line unicorn/no-useless-undefined
4522
4530
  setContextMenuFeature(undefined);
4523
4531
  } },
4524
4532
  React__default.createElement(MenuItem, { disabled: isReadOnly, key: 1, value: 1, onClick: () => {
@@ -4532,7 +4540,6 @@ function ApolloRendering(props) {
4532
4540
  session,
4533
4541
  handleClose: () => {
4534
4542
  doneCallback();
4535
- // eslint-disable-next-line unicorn/no-useless-undefined
4536
4543
  setContextMenuFeature(undefined);
4537
4544
  },
4538
4545
  changeManager,
@@ -4553,7 +4560,6 @@ function ApolloRendering(props) {
4553
4560
  session,
4554
4561
  handleClose: () => {
4555
4562
  doneCallback();
4556
- // eslint-disable-next-line unicorn/no-useless-undefined
4557
4563
  setContextMenuFeature(undefined);
4558
4564
  },
4559
4565
  changeManager,
@@ -4573,7 +4579,6 @@ function ApolloRendering(props) {
4573
4579
  session,
4574
4580
  handleClose: () => {
4575
4581
  doneCallback();
4576
- // eslint-disable-next-line unicorn/no-useless-undefined
4577
4582
  setContextMenuFeature(undefined);
4578
4583
  },
4579
4584
  changeManager,
@@ -4691,11 +4696,16 @@ function installApolloTextSearchAdapter(pluginManager) {
4691
4696
 
4692
4697
  const ApolloPluginConfigurationSchema = ConfigurationSchema('ApolloPlugin', {
4693
4698
  ontologies: types.array(OntologyRecordConfiguration),
4699
+ featureTypeOntologyName: {
4700
+ description: 'Name of the feature type ontology',
4701
+ type: 'string',
4702
+ defaultValue: 'Sequence Ontology',
4703
+ },
4694
4704
  });
4695
4705
 
4696
4706
  function parseCigar(cigar) {
4697
4707
  return (cigar.toUpperCase().match(/\d+\D/g) ?? []).map((op) => {
4698
- return [(op.match(/\D/) ?? [])[0], Number.parseInt(op, 10)];
4708
+ return [(/\D/.exec(op) ?? [])[0], Number.parseInt(op, 10)];
4699
4709
  });
4700
4710
  }
4701
4711
  function annotationFromPileup(pluggableElement) {
@@ -4867,7 +4877,7 @@ function annotationFromPileup(pluggableElement) {
4867
4877
  return pluggableElement;
4868
4878
  }
4869
4879
 
4870
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
4880
+ /* eslint-disable @typescript-eslint/unbound-method */
4871
4881
  const StringTextField = observer(function StringTextField({ onChangeCommitted, value: initialValue, ...props }) {
4872
4882
  const [value, setValue] = useState(String(initialValue));
4873
4883
  const [blur, setBlur] = useState(false);
@@ -5072,7 +5082,7 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
5072
5082
  }
5073
5083
  }
5074
5084
  return (React__default.createElement(React__default.Fragment, null,
5075
- React__default.createElement(Typography, { style: { display: 'inline', marginLeft: '15px' }, variant: "h5" }, "Attributes"),
5085
+ React__default.createElement(Typography, { variant: "h5" }, "Attributes"),
5076
5086
  React__default.createElement(Grid, { container: true, direction: "column", spacing: 1 },
5077
5087
  Object.entries(attributes).map(([key, value]) => {
5078
5088
  if (key === '') {
@@ -5121,7 +5131,7 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
5121
5131
  errorMessage ? (React__default.createElement(Typography, { color: "error" }, errorMessage)) : null));
5122
5132
  });
5123
5133
 
5124
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
5134
+ /* eslint-disable @typescript-eslint/unbound-method */
5125
5135
  const NumberTextField = observer(function NumberTextField({ onChangeCommitted, value: initialValue, ...props }) {
5126
5136
  const [value, setValue] = useState(String(initialValue));
5127
5137
  const [blur, setBlur] = useState(false);
@@ -5248,7 +5258,7 @@ const BasicInformation = observer(function BasicInformation({ assembly, feature,
5248
5258
  errorMessage ? (React__default.createElement(Typography, { color: "error" }, errorMessage)) : null));
5249
5259
  });
5250
5260
 
5251
- function formatSequence$1(seq, refName, start, end, wrap) {
5261
+ function formatSequence(seq, refName, start, end, wrap) {
5252
5262
  const header = `>${refName}:${start + 1}–${end}\n`;
5253
5263
  const body = wrap === undefined ? seq : splitStringIntoChunks(seq, wrap).join('\n');
5254
5264
  return `${header}${body}`;
@@ -5278,7 +5288,7 @@ const Sequence = observer(function Sequence({ assembly, feature, refName, sessio
5278
5288
  if (showSequence) {
5279
5289
  sequence = refSeq.getSequence(min, max);
5280
5290
  if (sequence) {
5281
- sequence = formatSequence$1(sequence, refName, min, max);
5291
+ sequence = formatSequence(sequence, refName, min, max);
5282
5292
  }
5283
5293
  else {
5284
5294
  void session.apolloDataStore.loadRefSeq([
@@ -5418,383 +5428,219 @@ const ApolloTranscriptDetailsModel = types
5418
5428
  },
5419
5429
  }));
5420
5430
 
5421
- /**
5422
- * Get single feature by featureId
5423
- * @param feature -
5424
- * @param featureId -
5425
- * @returns
5426
- */
5427
- function getFeatureFromId(feature, featureId) {
5428
- if (feature._id === featureId) {
5429
- return feature;
5430
- }
5431
- // Check if there is also childFeatures in parent feature and it's not empty
5432
- // Let's get featureId from recursive method
5433
- if (!feature.children) {
5434
- return;
5435
- }
5436
- for (const [, childFeature] of feature.children) {
5437
- const subFeature = getFeatureFromId(childFeature, featureId);
5438
- if (subFeature) {
5439
- return subFeature;
5440
- }
5441
- }
5442
- return;
5443
- }
5444
- function findExonInRange(exons, pairStart, pairEnd) {
5445
- for (const exon of exons) {
5446
- if (Number(exon.min) <= pairStart && Number(exon.max) >= pairEnd) {
5447
- return exon;
5448
- }
5449
- }
5450
- return null;
5451
- }
5452
- function removeMatchingExon(exons, matchStart, matchEnd) {
5453
- // Filter the array to remove elements matching the specified start and end
5454
- return exons.filter((exon) => !(exon.min === matchStart && exon.max === matchEnd));
5455
- }
5456
5431
  const TranscriptBasicInformation = observer(function TranscriptBasicInformation({ assembly, feature, refName, session, }) {
5457
5432
  const { notify } = session;
5458
5433
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5459
5434
  const refData = currentAssembly?.getByRefName(refName);
5460
5435
  const { changeManager } = session.apolloDataStore;
5461
- function handleStartChange(newStart, featureId, oldStart) {
5462
- newStart--;
5463
- oldStart--;
5464
- if (newStart < feature.min) {
5465
- notify('Feature start cannot be less than parent starts', 'error');
5466
- return;
5467
- }
5468
- const subFeature = getFeatureFromId(feature, featureId);
5469
- if (!subFeature?.children) {
5470
- return;
5436
+ const theme = useTheme();
5437
+ function handleLocationChange(oldLocation, newLocation, feature, isMin) {
5438
+ if (!feature.children) {
5439
+ throw new Error('Transcript should have child features');
5471
5440
  }
5472
- // Let's check CDS start and end values. And possibly update those too
5473
- for (const child of subFeature.children) {
5474
- if ((child[1].type === 'CDS' || child[1].type === 'exon') &&
5475
- child[1].min === oldStart) {
5441
+ for (const [, child] of feature.children) {
5442
+ if (isMin && oldLocation - 1 === child.min) {
5476
5443
  const change = new LocationStartChange({
5477
5444
  typeName: 'LocationStartChange',
5478
- changedIds: [child[1]._id],
5479
- featureId,
5480
- oldStart,
5481
- newStart,
5445
+ changedIds: [child._id],
5446
+ featureId: feature._id,
5447
+ oldStart: oldLocation - 1,
5448
+ newStart: newLocation - 1,
5482
5449
  assembly,
5483
5450
  });
5484
5451
  changeManager.submit(change).catch(() => {
5485
5452
  notify('Error updating feature start position', 'error');
5486
5453
  });
5454
+ return;
5487
5455
  }
5488
- }
5489
- return;
5490
- }
5491
- function handleEndChange(newEnd, featureId, oldEnd) {
5492
- const subFeature = getFeatureFromId(feature, featureId);
5493
- if (newEnd > feature.max) {
5494
- notify('Feature start cannot be greater than parent end', 'error');
5495
- return;
5496
- }
5497
- if (!subFeature?.children) {
5498
- return;
5499
- }
5500
- // Let's check CDS start and end values. And possibly update those too
5501
- for (const child of subFeature.children) {
5502
- if ((child[1].type === 'CDS' || child[1].type === 'exon') &&
5503
- child[1].max === oldEnd) {
5456
+ if (!isMin && newLocation === child.max) {
5504
5457
  const change = new LocationEndChange({
5505
5458
  typeName: 'LocationEndChange',
5506
- changedIds: [child[1]._id],
5507
- featureId,
5508
- oldEnd,
5509
- newEnd,
5459
+ changedIds: [child._id],
5460
+ featureId: feature._id,
5461
+ oldEnd: child.max,
5462
+ newEnd: newLocation,
5510
5463
  assembly,
5511
5464
  });
5512
5465
  changeManager.submit(change).catch(() => {
5513
- notify('Error updating feature end position', 'error');
5466
+ notify('Error updating feature start position', 'error');
5514
5467
  });
5468
+ return;
5515
5469
  }
5516
5470
  }
5517
- return;
5518
5471
  }
5519
- const featureNew = feature;
5520
- let exonsArray = [];
5521
- const traverse = (currentFeature) => {
5522
- if (currentFeature.type === 'exon') {
5523
- exonsArray.push({
5524
- min: currentFeature.min + 1,
5525
- max: currentFeature.max,
5526
- });
5527
- }
5528
- if (currentFeature.children) {
5529
- for (const child of currentFeature.children) {
5530
- traverse(child[1]);
5531
- }
5532
- }
5533
- };
5534
- traverse(featureNew);
5535
- const CDSresult = [];
5536
- const CDSData = featureNew.cdsLocations;
5537
- if (refData) {
5538
- for (const CDSDatum of CDSData) {
5539
- for (const dataPoint of CDSDatum) {
5540
- let startSeq = refData.getSequence(Number(dataPoint.min) - 2, Number(dataPoint.min));
5541
- let endSeq = refData.getSequence(Number(dataPoint.max), Number(dataPoint.max) + 2);
5542
- if (featureNew.strand === -1 && startSeq && endSeq) {
5543
- startSeq = revcom(startSeq);
5544
- endSeq = revcom(endSeq);
5545
- }
5546
- const oneCDS = {
5547
- id: featureNew._id,
5548
- type: 'CDS',
5549
- strand: Number(featureNew.strand),
5550
- min: dataPoint.min + 1,
5551
- max: dataPoint.max,
5552
- oldMin: dataPoint.min + 1,
5553
- oldMax: dataPoint.max,
5554
- startSeq,
5555
- endSeq,
5556
- };
5557
- // CDSresult.push(oneCDS)
5558
- // Check if there is already an object with the same start and end
5559
- const exists = CDSresult.some((obj) => obj.min === oneCDS.min &&
5560
- obj.max === oneCDS.max &&
5561
- obj.type === oneCDS.type);
5562
- // If no such object exists, add the new object to the array
5563
- if (!exists) {
5564
- CDSresult.push(oneCDS);
5565
- }
5566
- // Add possible UTRs
5567
- const foundExon = findExonInRange(exonsArray, dataPoint.min + 1, dataPoint.max);
5568
- if (foundExon && Number(foundExon.min) < dataPoint.min) {
5569
- if (feature.strand === 1) {
5570
- const oneCDS = {
5571
- id: feature._id,
5572
- type: 'five_prime_UTR',
5573
- strand: Number(feature.strand),
5574
- min: foundExon.min,
5575
- max: dataPoint.min,
5576
- oldMin: foundExon.min,
5577
- oldMax: dataPoint.min,
5578
- startSeq: '',
5579
- endSeq: '',
5580
- };
5581
- CDSresult.push(oneCDS);
5582
- }
5583
- else {
5584
- const oneCDS = {
5585
- id: feature._id,
5586
- type: 'three_prime_UTR',
5587
- strand: Number(feature.strand),
5588
- min: dataPoint.min + 1,
5589
- max: foundExon.min + 1,
5590
- oldMin: dataPoint.min + 1,
5591
- oldMax: foundExon.min + 1,
5592
- startSeq: '',
5593
- endSeq: '',
5594
- };
5595
- CDSresult.push(oneCDS);
5596
- }
5597
- exonsArray = removeMatchingExon(exonsArray, foundExon.min, foundExon.max);
5598
- }
5599
- if (foundExon && Number(foundExon.max) > dataPoint.max) {
5600
- if (feature.strand === 1) {
5601
- const oneCDS = {
5602
- id: feature._id,
5603
- type: 'three_prime_UTR',
5604
- strand: Number(feature.strand),
5605
- min: dataPoint.max + 1,
5606
- max: foundExon.max,
5607
- oldMin: dataPoint.max + 1,
5608
- oldMax: foundExon.max,
5609
- startSeq: '',
5610
- endSeq: '',
5611
- };
5612
- CDSresult.push(oneCDS);
5613
- }
5614
- else {
5615
- const oneCDS = {
5616
- id: feature._id,
5617
- type: 'five_prime_UTR',
5618
- strand: Number(feature.strand),
5619
- min: dataPoint.min + 1,
5620
- max: foundExon.max,
5621
- oldMin: dataPoint.min + 1,
5622
- oldMax: foundExon.max,
5623
- startSeq: '',
5624
- endSeq: '',
5625
- };
5626
- CDSresult.push(oneCDS);
5627
- }
5628
- exonsArray = removeMatchingExon(exonsArray, foundExon.min, foundExon.max);
5629
- }
5630
- if (dataPoint.min + 1 === foundExon?.min &&
5631
- dataPoint.max === foundExon.max) {
5632
- exonsArray = removeMatchingExon(exonsArray, foundExon.min, foundExon.max);
5633
- }
5634
- }
5635
- }
5636
- }
5637
- // Add remaining UTRs if any
5638
- if (exonsArray.length > 0) {
5639
- // eslint-disable-next-line unicorn/no-array-for-each
5640
- exonsArray.forEach((element) => {
5641
- if (featureNew.strand === 1) {
5642
- const oneCDS = {
5643
- id: featureNew._id,
5644
- type: 'five_prime_UTR',
5645
- strand: Number(featureNew.strand),
5646
- min: element.min + 1,
5647
- max: element.max,
5648
- oldMin: element.min + 1,
5649
- oldMax: element.max,
5650
- startSeq: '',
5651
- endSeq: '',
5652
- };
5653
- CDSresult.push(oneCDS);
5654
- }
5655
- else {
5656
- const oneCDS = {
5657
- id: featureNew._id,
5658
- type: 'three_prime_UTR',
5659
- strand: Number(featureNew.strand),
5660
- min: element.min + 1,
5661
- max: element.max + 1,
5662
- oldMin: element.min + 1,
5663
- oldMax: element.max + 1,
5664
- startSeq: '',
5665
- endSeq: '',
5666
- };
5667
- CDSresult.push(oneCDS);
5668
- }
5669
- exonsArray = removeMatchingExon(exonsArray, element.min, element.max);
5670
- });
5472
+ if (!refData) {
5473
+ return null;
5671
5474
  }
5672
- CDSresult.sort((a, b) => {
5673
- // Primary sorting by 'start' property
5674
- const startDifference = Number(a.min) - Number(b.min);
5675
- if (startDifference !== 0) {
5676
- return startDifference;
5677
- }
5678
- return Number(a.max) - Number(b.max);
5679
- });
5680
- if (CDSresult.length > 0) {
5681
- CDSresult[0].startSeq = '';
5682
- // eslint-disable-next-line unicorn/prefer-at
5683
- CDSresult[CDSresult.length - 1].endSeq = '';
5684
- // Loop through the array and clear "startSeq" or "endSeq" based on the conditions
5685
- for (let i = 0; i < CDSresult.length; i++) {
5686
- if (i > 0 && CDSresult[i].min === CDSresult[i - 1].max) {
5687
- // Clear "startSeq" if the current item's "start" is equal to the previous item's "end"
5688
- CDSresult[i].startSeq = '';
5475
+ const { strand, transcriptParts } = feature;
5476
+ const [firstLocation] = transcriptParts;
5477
+ const locationData = firstLocation
5478
+ .map((loc, idx) => {
5479
+ const { max, min, type } = loc;
5480
+ let label = type;
5481
+ if (label === 'threePrimeUTR') {
5482
+ label = '3` UTR';
5483
+ }
5484
+ else if (label === 'fivePrimeUTR') {
5485
+ label = '5` UTR';
5486
+ }
5487
+ let fivePrimeSpliceSite;
5488
+ let threePrimeSpliceSite;
5489
+ let frameColor;
5490
+ if (type === 'CDS') {
5491
+ const { phase } = loc;
5492
+ const frame = getFrame(min, max, strand ?? 1, phase);
5493
+ frameColor = theme.palette.framesCDS.at(frame)?.main;
5494
+ const previousLoc = firstLocation.at(idx - 1);
5495
+ const nextLoc = firstLocation.at(idx + 1);
5496
+ if (strand === 1) {
5497
+ if (previousLoc?.type === 'intron') {
5498
+ fivePrimeSpliceSite = refData.getSequence(min - 2, min);
5499
+ }
5500
+ if (nextLoc?.type === 'intron') {
5501
+ threePrimeSpliceSite = refData.getSequence(max, max + 2);
5502
+ }
5689
5503
  }
5690
- if (i < CDSresult.length - 1 &&
5691
- CDSresult[i].max === CDSresult[i + 1].min) {
5692
- // Clear "endSeq" if the next item's "start" is equal to the current item's "end"
5693
- CDSresult[i].endSeq = '';
5504
+ else {
5505
+ if (previousLoc?.type === 'intron') {
5506
+ fivePrimeSpliceSite = revcom(refData.getSequence(max, max + 2));
5507
+ }
5508
+ if (nextLoc?.type === 'intron') {
5509
+ threePrimeSpliceSite = revcom(refData.getSequence(min - 2, min));
5510
+ }
5694
5511
  }
5695
5512
  }
5696
- }
5697
- const transcriptItems = CDSresult;
5513
+ return {
5514
+ min,
5515
+ max,
5516
+ label,
5517
+ fivePrimeSpliceSite,
5518
+ threePrimeSpliceSite,
5519
+ frameColor,
5520
+ };
5521
+ })
5522
+ .filter((loc) => loc.label !== 'intron');
5698
5523
  return (React__default.createElement(React__default.Fragment, null,
5699
- React__default.createElement(Typography, { variant: "h5", style: { marginLeft: '15px', marginBottom: '0' } }, "CDS and UTRs"),
5700
- React__default.createElement("div", null, transcriptItems.map((item, index) => (React__default.createElement("div", { key: index, style: { display: 'flex', alignItems: 'center' } },
5701
- React__default.createElement("span", { style: { marginLeft: '20px', width: '50px' } }, item.type === 'three_prime_UTR'
5702
- ? '3 UTR'
5703
- : // eslint-disable-next-line unicorn/no-nested-ternary
5704
- item.type === 'five_prime_UTR'
5705
- ? '5 UTR'
5706
- : 'CDS'),
5707
- React__default.createElement("span", { style: { fontWeight: 'bold', width: '30px' } }, item.startSeq),
5708
- React__default.createElement(NumberTextField, { margin: "dense", id: item.id, disabled: item.type !== 'CDS', style: {
5709
- width: '150px',
5710
- marginLeft: '8px',
5711
- backgroundColor: item.startSeq.trim() === '' && index !== 0
5712
- ? 'lightblue'
5713
- : 'inherit',
5714
- }, variant: "outlined", value: item.min, onChangeCommitted: (newStart) => {
5715
- handleStartChange(newStart, item.id, Number(item.oldMin));
5716
- } }),
5717
- React__default.createElement("span", { style: { margin: '0 10px' } }, item.strand === -1 ? '-' : item.strand === 1 ? '+' : ''),
5718
- React__default.createElement(NumberTextField, { margin: "dense", id: item.id, disabled: item.type !== 'CDS', style: {
5719
- width: '150px',
5720
- backgroundColor: item.endSeq.trim() === '' &&
5721
- index + 1 !== transcriptItems.length
5722
- ? 'lightblue'
5723
- : 'inherit',
5724
- }, variant: "outlined", value: item.max, onChangeCommitted: (newEnd) => {
5725
- handleEndChange(newEnd, item.id, Number(item.oldMax));
5726
- } }),
5727
- React__default.createElement("span", { style: { marginLeft: '8px', fontWeight: 'bold' } }, item.endSeq)))))));
5524
+ React__default.createElement(Typography, { variant: "h5" }, "Structure"),
5525
+ React__default.createElement(Typography, { variant: "h6" },
5526
+ strand === 1 ? 'Forward' : 'Reverse',
5527
+ " strand"),
5528
+ React__default.createElement(TableContainer, { component: Paper },
5529
+ React__default.createElement(Table, { size: "small" },
5530
+ React__default.createElement(TableBody, null, locationData.map((loc) => (React__default.createElement(TableRow, { key: `${loc.label}:${loc.min}-${loc.max}` },
5531
+ React__default.createElement(TableCell, { component: "th", scope: "row", style: { background: loc.frameColor } }, loc.label),
5532
+ React__default.createElement(TableCell, null, loc.fivePrimeSpliceSite ?? ''),
5533
+ React__default.createElement(TableCell, { padding: "none" },
5534
+ React__default.createElement(NumberTextField, { margin: "dense", variant: "outlined", value: strand === 1 ? loc.min + 1 : loc.max, onChangeCommitted: (newLocation) => {
5535
+ handleLocationChange(strand === 1 ? loc.min + 1 : loc.max, newLocation, feature, strand === 1);
5536
+ } })),
5537
+ React__default.createElement(TableCell, { padding: "none" },
5538
+ React__default.createElement(NumberTextField, { margin: "dense",
5539
+ // disabled={item.type !== 'CDS'}
5540
+ variant: "outlined", value: strand === 1 ? loc.max : loc.min + 1, onChangeCommitted: (newLocation) => {
5541
+ handleLocationChange(strand === 1 ? loc.max : loc.min + 1, newLocation, feature, strand !== 1);
5542
+ } })),
5543
+ React__default.createElement(TableCell, null, loc.threePrimeSpliceSite ?? '')))))))));
5728
5544
  });
5729
5545
 
5730
- const getCDSInfo = (feature, refData) => {
5731
- const CDSresult = [];
5732
- const traverse = (currentFeature, isParentMRNA) => {
5733
- if (isParentMRNA &&
5734
- (currentFeature.type === 'CDS' ||
5735
- currentFeature.type === 'three_prime_UTR' ||
5736
- currentFeature.type === 'five_prime_UTR')) {
5737
- let startSeq = refData.getSequence(Number(currentFeature.min) - 2, Number(currentFeature.min));
5738
- let endSeq = refData.getSequence(Number(currentFeature.max), Number(currentFeature.max) + 2);
5739
- if (currentFeature.strand === -1 && startSeq && endSeq) {
5740
- startSeq = revcom(startSeq);
5741
- endSeq = revcom(endSeq);
5742
- }
5743
- const oneCDS = {
5744
- id: currentFeature._id,
5745
- type: currentFeature.type,
5746
- strand: Number(currentFeature.strand),
5747
- min: currentFeature.min + 1,
5748
- max: currentFeature.max + 1,
5749
- oldMin: currentFeature.min + 1,
5750
- oldMax: currentFeature.max + 1,
5751
- startSeq: startSeq || '',
5752
- endSeq: endSeq || '',
5753
- };
5754
- CDSresult.push(oneCDS);
5755
- }
5756
- if (currentFeature.children) {
5757
- for (const child of currentFeature.children) {
5758
- traverse(child[1], feature.type === 'mRNA');
5546
+ const SEQUENCE_WRAP_LENGTH = 60;
5547
+ function getSequenceSegments(segmentType, feature, getSequence) {
5548
+ const segments = [];
5549
+ const { cdsLocations, strand, transcriptParts } = feature;
5550
+ switch (segmentType) {
5551
+ case 'genomic':
5552
+ case 'cDNA': {
5553
+ const [firstLocation] = transcriptParts;
5554
+ for (const loc of firstLocation) {
5555
+ if (segmentType === 'cDNA' && loc.type === 'intron') {
5556
+ continue;
5557
+ }
5558
+ let sequence = getSequence(loc.min, loc.max);
5559
+ if (strand === -1) {
5560
+ sequence = revcom(sequence);
5561
+ }
5562
+ const type = loc.type === 'fivePrimeUTR' || loc.type === 'threePrimeUTR'
5563
+ ? 'UTR'
5564
+ : loc.type;
5565
+ const previousSegment = segments.at(-1);
5566
+ if (!previousSegment) {
5567
+ const sequenceLines = splitStringIntoChunks(sequence, SEQUENCE_WRAP_LENGTH);
5568
+ segments.push({
5569
+ type,
5570
+ sequenceLines,
5571
+ locs: [{ min: loc.min, max: loc.max }],
5572
+ });
5573
+ continue;
5574
+ }
5575
+ if (previousSegment.type === type) {
5576
+ const [previousSegmentFirstLine, ...previousSegmentFollowingLines] = previousSegment.sequenceLines;
5577
+ const newSequence = previousSegmentFollowingLines.join('') + sequence;
5578
+ previousSegment.sequenceLines = [
5579
+ previousSegmentFirstLine,
5580
+ ...splitStringIntoChunks(newSequence, SEQUENCE_WRAP_LENGTH),
5581
+ ];
5582
+ previousSegment.locs.push({ min: loc.min, max: loc.max });
5583
+ }
5584
+ else {
5585
+ const count = segments.reduce((accumulator, currentSegment) => accumulator +
5586
+ currentSegment.sequenceLines.reduce((subAccumulator, currentLine) => subAccumulator + currentLine.length, 0), 0);
5587
+ const previousLineLength = count % SEQUENCE_WRAP_LENGTH;
5588
+ const newSegmentFirstLineLength = SEQUENCE_WRAP_LENGTH - previousLineLength;
5589
+ const newSegmentFirstLine = sequence.slice(0, newSegmentFirstLineLength);
5590
+ const newSegmentRemainderLines = splitStringIntoChunks(sequence.slice(newSegmentFirstLineLength), SEQUENCE_WRAP_LENGTH);
5591
+ segments.push({
5592
+ type,
5593
+ sequenceLines: [newSegmentFirstLine, ...newSegmentRemainderLines],
5594
+ locs: [{ min: loc.min, max: loc.max }],
5595
+ });
5596
+ }
5759
5597
  }
5598
+ return segments;
5760
5599
  }
5761
- };
5762
- traverse(feature, feature.type === 'mRNA');
5763
- CDSresult.sort((a, b) => {
5764
- return Number(a.min) - Number(b.min);
5765
- });
5766
- if (CDSresult.length > 0) {
5767
- CDSresult[0].startSeq = '';
5768
- // eslint-disable-next-line unicorn/prefer-at
5769
- CDSresult[CDSresult.length - 1].endSeq = '';
5770
- // Loop through the array and clear "startSeq" or "endSeq" based on the conditions
5771
- for (let i = 0; i < CDSresult.length; i++) {
5772
- if (i > 0 && CDSresult[i].min === CDSresult[i - 1].max) {
5773
- // Clear "startSeq" if the current item's "start" is equal to the previous item's "end"
5774
- CDSresult[i].startSeq = '';
5775
- }
5776
- if (i < CDSresult.length - 1 &&
5777
- CDSresult[i].max === CDSresult[i + 1].min) {
5778
- // Clear "endSeq" if the next item's "start" is equal to the current item's "end"
5779
- CDSresult[i].endSeq = '';
5600
+ case 'CDS': {
5601
+ let wholeSequence = '';
5602
+ const [firstLocation] = cdsLocations;
5603
+ const locs = [];
5604
+ for (const loc of firstLocation) {
5605
+ let sequence = getSequence(loc.min, loc.max);
5606
+ if (strand === -1) {
5607
+ sequence = revcom(sequence);
5608
+ }
5609
+ wholeSequence += sequence;
5610
+ locs.push({ min: loc.min, max: loc.max });
5780
5611
  }
5612
+ const sequenceLines = splitStringIntoChunks(wholeSequence, SEQUENCE_WRAP_LENGTH);
5613
+ segments.push({ type: 'CDS', sequenceLines, locs });
5614
+ return segments;
5615
+ }
5616
+ }
5617
+ }
5618
+ function getSegmentColor(type) {
5619
+ switch (type) {
5620
+ case 'upOrDownstream': {
5621
+ return 'rgb(255,255,255)';
5622
+ }
5623
+ case 'UTR': {
5624
+ return 'rgb(194,106,119)';
5625
+ }
5626
+ case 'CDS': {
5627
+ return 'rgb(93,168,153)';
5628
+ }
5629
+ case 'intron': {
5630
+ return 'rgb(187,187,187)';
5631
+ }
5632
+ case 'protein': {
5633
+ return 'rgb(148,203,236)';
5781
5634
  }
5782
5635
  }
5783
- return CDSresult;
5784
- };
5785
- function formatSequence(seq, refName, start, end, wrap) {
5786
- const header = `>${refName}:${start + 1}–${end}\n`;
5787
- const body = wrap === undefined ? seq : splitStringIntoChunks(seq, wrap).join('\n');
5788
- return `${header}${body}`;
5789
5636
  }
5790
- const utrColor = 'rgb(20,200,200)'; // Slightly brighter cyan
5791
- const cdsColor = 'rgb(240,200,20)'; // Slightly brighter yellow
5792
- let textSegments = [{ text: '', color: '' }];
5793
5637
  const TranscriptSequence = observer(function TranscriptSequence({ assembly, feature, refName, session, }) {
5794
5638
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5795
5639
  const refData = currentAssembly?.getByRefName(refName);
5796
5640
  const [showSequence, setShowSequence] = useState(false);
5797
- const [selectedOption, setSelectedOption] = useState('Select');
5641
+ const [selectedOption, setSelectedOption] = useState('CDS');
5642
+ const theme = useTheme();
5643
+ const seqRef = useRef(null);
5798
5644
  if (!(currentAssembly && refData)) {
5799
5645
  return null;
5800
5646
  }
@@ -5802,150 +5648,85 @@ const TranscriptSequence = observer(function TranscriptSequence({ assembly, feat
5802
5648
  if (!refSeq) {
5803
5649
  return null;
5804
5650
  }
5805
- const transcriptItems = getCDSInfo(feature, refData);
5806
- const { max, min } = feature;
5807
- let sequence = '';
5808
- if (showSequence) {
5809
- getSequenceAsString(min, max);
5810
- }
5811
- function getSequenceAsString(start, end) {
5812
- sequence = refSeq?.getSequence(start, end) ?? '';
5813
- if (sequence === '') {
5814
- void session.apolloDataStore.loadRefSeq([
5815
- { assemblyName: assembly, refName, start, end },
5816
- ]);
5817
- }
5818
- else {
5819
- sequence = formatSequence(sequence, refName, start, end);
5820
- }
5821
- getSequenceAsTextSegment(selectedOption); // For color coded sequence
5822
- return sequence;
5651
+ if (feature.type !== 'mRNA') {
5652
+ return null;
5823
5653
  }
5824
5654
  const handleSeqButtonClick = () => {
5825
5655
  setShowSequence(!showSequence);
5826
5656
  };
5827
- function getSequenceAsTextSegment(option) {
5828
- let seqData = '';
5829
- textSegments = [];
5830
- if (!refData) {
5831
- return;
5832
- }
5833
- switch (option) {
5834
- case 'CDS': {
5835
- textSegments.push({ text: `>${refName} : CDS\n`, color: 'black' });
5836
- for (const item of transcriptItems) {
5837
- if (item.type === 'CDS') {
5838
- const refSeq = refData.getSequence(Number(item.min + 1), Number(item.max));
5839
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq;
5840
- textSegments.push({ text: seqData, color: cdsColor });
5841
- }
5842
- }
5843
- break;
5844
- }
5845
- case 'cDNA': {
5846
- textSegments.push({ text: `>${refName} : cDNA\n`, color: 'black' });
5847
- for (const item of transcriptItems) {
5848
- if (item.type === 'CDS' ||
5849
- item.type === 'three_prime_UTR' ||
5850
- item.type === 'five_prime_UTR') {
5851
- const refSeq = refData.getSequence(Number(item.min + 1), Number(item.max));
5852
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq;
5853
- if (item.type === 'CDS') {
5854
- textSegments.push({ text: seqData, color: cdsColor });
5855
- }
5856
- else {
5857
- textSegments.push({ text: seqData, color: utrColor });
5858
- }
5859
- }
5860
- }
5861
- break;
5862
- }
5863
- case 'Full': {
5864
- textSegments.push({
5865
- text: `>${refName} : Full genomic\n`,
5866
- color: 'black',
5867
- });
5868
- let lastEnd = 0;
5869
- let count = 0;
5870
- for (const item of transcriptItems) {
5871
- count++;
5872
- if (lastEnd != 0 &&
5873
- lastEnd != Number(item.min) &&
5874
- count != transcriptItems.length) {
5875
- // Intron etc. between CDS/UTRs. No need to check this on very last item
5876
- const refSeq = refData.getSequence(lastEnd + 1, Number(item.min) - 1);
5877
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq;
5878
- textSegments.push({ text: seqData, color: 'black' });
5879
- }
5880
- if (item.type === 'CDS' ||
5881
- item.type === 'three_prime_UTR' ||
5882
- item.type === 'five_prime_UTR') {
5883
- const refSeq = refData.getSequence(Number(item.min + 1), Number(item.max));
5884
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq;
5885
- switch (item.type) {
5886
- case 'CDS': {
5887
- textSegments.push({ text: seqData, color: cdsColor });
5888
- break;
5889
- }
5890
- case 'three_prime_UTR': {
5891
- textSegments.push({ text: seqData, color: utrColor });
5892
- break;
5893
- }
5894
- case 'five_prime_UTR': {
5895
- textSegments.push({ text: seqData, color: utrColor });
5896
- break;
5897
- }
5898
- default: {
5899
- textSegments.push({ text: seqData, color: 'black' });
5900
- break;
5901
- }
5902
- }
5903
- }
5904
- lastEnd = Number(item.max);
5905
- }
5906
- break;
5907
- }
5908
- }
5909
- }
5910
5657
  function handleChangeSeqOption(e) {
5911
5658
  const option = e.target.value;
5912
5659
  setSelectedOption(option);
5913
- getSequenceAsTextSegment(option);
5914
5660
  }
5915
5661
  // Function to copy text to clipboard
5916
5662
  const copyToClipboard = () => {
5917
- const textToCopy = textSegments.map((segment) => segment.text).join('');
5918
- if (textToCopy) {
5919
- navigator.clipboard
5920
- .writeText(textToCopy)
5921
- .then(() => {
5922
- // console.log('Text copied to clipboard!')
5923
- })
5924
- .catch((error) => {
5925
- console.error('Failed to copy text to clipboard', error);
5926
- });
5663
+ const seqDiv = seqRef.current;
5664
+ if (!seqDiv) {
5665
+ return;
5927
5666
  }
5667
+ const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' });
5668
+ const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
5669
+ const clipboardItem = new ClipboardItem({
5670
+ [textBlob.type]: textBlob,
5671
+ [htmlBlob.type]: htmlBlob,
5672
+ });
5673
+ void navigator.clipboard.write([clipboardItem]);
5928
5674
  };
5929
- const ColoredText = ({ textSegments }) => {
5930
- return (React__default.createElement("div", null, textSegments.map((segment, index) => (React__default.createElement("span", { key: index, style: { color: segment.color } }, splitStringIntoChunks(segment.text, 150).join('\n'))))));
5931
- };
5675
+ const sequenceSegments = showSequence
5676
+ ? getSequenceSegments(selectedOption, feature, (min, max) => refData.getSequence(min, max))
5677
+ : [];
5678
+ const locationIntervals = [];
5679
+ if (showSequence) {
5680
+ const allLocs = sequenceSegments.flatMap((segment) => segment.locs);
5681
+ let [previous] = allLocs;
5682
+ for (let i = 1; i < allLocs.length; i++) {
5683
+ if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
5684
+ previous = {
5685
+ min: Math.min(previous.min, allLocs[i].min),
5686
+ max: Math.max(previous.max, allLocs[i].max),
5687
+ };
5688
+ }
5689
+ else {
5690
+ locationIntervals.push(previous);
5691
+ previous = allLocs[i];
5692
+ }
5693
+ }
5694
+ locationIntervals.push(previous);
5695
+ }
5932
5696
  return (React__default.createElement(React__default.Fragment, null,
5933
- React__default.createElement(Typography, { style: { display: 'inline', marginLeft: '15px' }, variant: "h5" }, "Sequence"),
5697
+ React__default.createElement(Typography, { variant: "h5" }, "Sequence"),
5934
5698
  React__default.createElement("div", null,
5935
- React__default.createElement(Button, { variant: "contained", style: { marginLeft: '15px' }, onClick: handleSeqButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence')),
5936
- React__default.createElement("div", null, showSequence && (React__default.createElement(Select, { value: selectedOption, onChange: handleChangeSeqOption, style: { width: '150px', marginLeft: '15px', height: '25px' } },
5937
- React__default.createElement(MenuItem, { value: 'Select' }, "Select"),
5938
- React__default.createElement(MenuItem, { value: 'CDS' }, "CDS"),
5939
- React__default.createElement(MenuItem, { value: 'cDNA' }, "cDNA"),
5940
- React__default.createElement(MenuItem, { value: 'Full' }, "Full genomics")))),
5941
- React__default.createElement("div", { style: {
5942
- width: '500px',
5943
- marginLeft: '15px',
5944
- height: '300px',
5945
- overflowY: 'auto',
5946
- border: '1px solid #ccc',
5947
- } }, showSequence && React__default.createElement(ColoredText, { textSegments: textSegments })),
5948
- showSequence && (React__default.createElement(Button, { variant: "contained", style: { marginLeft: '15px' }, onClick: copyToClipboard }, "Copy sequence"))));
5699
+ React__default.createElement(Button, { variant: "contained", onClick: handleSeqButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence')),
5700
+ showSequence && (React__default.createElement(React__default.Fragment, null,
5701
+ React__default.createElement(Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
5702
+ React__default.createElement(MenuItem, { value: "CDS" }, "CDS"),
5703
+ React__default.createElement(MenuItem, { value: "cDNA" }, "cDNA"),
5704
+ React__default.createElement(MenuItem, { value: "genomic" }, "Genomic")),
5705
+ React__default.createElement(Paper, { style: {
5706
+ fontFamily: 'monospace',
5707
+ padding: theme.spacing(),
5708
+ overflowX: 'auto',
5709
+ }, ref: seqRef },
5710
+ ">",
5711
+ refSeq.name,
5712
+ ":",
5713
+ locationIntervals
5714
+ .map((interval) => feature.strand === 1
5715
+ ? `${interval.min + 1}-${interval.max}`
5716
+ : `${interval.max}-${interval.min + 1}`)
5717
+ .join(';'),
5718
+ "(",
5719
+ feature.strand === 1 ? '+' : '-',
5720
+ ")",
5721
+ React__default.createElement("br", null),
5722
+ sequenceSegments.map((segment, index) => (React__default.createElement("span", { key: `${segment.type}-${index}`, style: {
5723
+ background: getSegmentColor(segment.type),
5724
+ color: theme.palette.getContrastText(getSegmentColor(segment.type)),
5725
+ } }, segment.sequenceLines.map((sequenceLine, idx) => (React__default.createElement(React__default.Fragment, { key: `${sequenceLine.slice(0, 5)}-${idx}` },
5726
+ sequenceLine,
5727
+ idx === segment.sequenceLines.length - 1 &&
5728
+ sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (React__default.createElement("br", null))))))))),
5729
+ React__default.createElement(Button, { variant: "contained", onClick: copyToClipboard }, "Copy sequence")))));
5949
5730
  });
5950
5731
 
5951
5732
  const useStyles$7 = makeStyles()((theme) => ({
@@ -6176,7 +5957,7 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
6176
5957
  return menuItems;
6177
5958
  }
6178
5959
 
6179
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
5960
+ /* eslint-disable @typescript-eslint/unbound-method */
6180
5961
  const useStyles$5 = makeStyles()((theme) => ({
6181
5962
  inputWrapper: {
6182
5963
  position: 'relative',
@@ -6405,7 +6186,9 @@ function baseModelFactory(_pluginManager, configSchema) {
6405
6186
  return;
6406
6187
  }
6407
6188
  void self.session.apolloDataStore.loadFeatures(self.regions);
6408
- void self.session.apolloDataStore.loadRefSeq(self.regions);
6189
+ if (self.lgv.bpPerPx <= 3) {
6190
+ void self.session.apolloDataStore.loadRefSeq(self.regions);
6191
+ }
6409
6192
  }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
6410
6193
  },
6411
6194
  }));
@@ -7290,7 +7073,7 @@ function drawTooltip$2(display, context) {
7290
7073
  if (!position) {
7291
7074
  return;
7292
7075
  }
7293
- const { layoutIndex, layoutRow } = position;
7076
+ const { featureRow, layoutIndex, layoutRow } = position;
7294
7077
  const { bpPerPx, displayedRegions, offsetPx } = lgv;
7295
7078
  const displayedRegion = displayedRegions[layoutIndex];
7296
7079
  const { refName, reversed } = displayedRegion;
@@ -7302,7 +7085,7 @@ function drawTooltip$2(display, context) {
7302
7085
  coord: reversed ? max : min,
7303
7086
  regionNumber: layoutIndex,
7304
7087
  })?.offsetPx ?? 0) - offsetPx;
7305
- const top = layoutRow * apolloRowHeight;
7088
+ const top = (layoutRow + featureRow) * apolloRowHeight;
7306
7089
  const widthPx = length / bpPerPx;
7307
7090
  const featureType = `Type: ${feature.type}`;
7308
7091
  const { attributes } = feature;
@@ -7448,6 +7231,20 @@ function getContextMenuItems$2(display) {
7448
7231
  session.showWidget(apolloFeatureWidget);
7449
7232
  },
7450
7233
  });
7234
+ if (sourceFeature.type === 'mRNA' && isSessionModelWithWidgets(session)) {
7235
+ menuItems.push({
7236
+ label: 'Edit transcript details',
7237
+ onClick: () => {
7238
+ const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
7239
+ feature: sourceFeature,
7240
+ assembly: currentAssemblyId,
7241
+ changeManager,
7242
+ refName: region.refName,
7243
+ });
7244
+ session.showWidget(apolloTranscriptWidget);
7245
+ },
7246
+ });
7247
+ }
7451
7248
  return menuItems;
7452
7249
  }
7453
7250
  function getFeatureFromLayout$2(feature, _bp, _row) {
@@ -7532,35 +7329,49 @@ const boxGlyph = {
7532
7329
  onMouseUp: onMouseUp$2,
7533
7330
  };
7534
7331
 
7535
- let forwardFill = null;
7536
- let backwardFill = null;
7537
- if ('document' in window) {
7332
+ let forwardFillLight = null;
7333
+ let backwardFillLight = null;
7334
+ let forwardFillDark = null;
7335
+ let backwardFillDark = null;
7336
+ if ('document' in globalThis) {
7538
7337
  for (const direction of ['forward', 'backward']) {
7539
- const canvas = document.createElement('canvas');
7540
- const canvasSize = 10;
7541
- canvas.width = canvas.height = canvasSize;
7542
- const ctx = canvas.getContext('2d');
7543
- if (ctx) {
7544
- const stripeColor1 = 'rgba(0,0,0,0)';
7545
- const stripeColor2 = 'rgba(255,255,255,0.25)';
7546
- const gradient = direction === 'forward'
7547
- ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
7548
- : ctx.createLinearGradient(0, 0, canvasSize, canvasSize);
7549
- gradient.addColorStop(0, stripeColor1);
7550
- gradient.addColorStop(0.25, stripeColor1);
7551
- gradient.addColorStop(0.25, stripeColor2);
7552
- gradient.addColorStop(0.5, stripeColor2);
7553
- gradient.addColorStop(0.5, stripeColor1);
7554
- gradient.addColorStop(0.75, stripeColor1);
7555
- gradient.addColorStop(0.75, stripeColor2);
7556
- gradient.addColorStop(1, stripeColor2);
7557
- ctx.fillStyle = gradient;
7558
- ctx.fillRect(0, 0, 10, 10);
7559
- if (direction === 'forward') {
7560
- forwardFill = ctx.createPattern(canvas, 'repeat');
7561
- }
7562
- else {
7563
- backwardFill = ctx.createPattern(canvas, 'repeat');
7338
+ for (const themeMode of ['light', 'dark']) {
7339
+ const canvas = document.createElement('canvas');
7340
+ const canvasSize = 10;
7341
+ canvas.width = canvas.height = canvasSize;
7342
+ const ctx = canvas.getContext('2d');
7343
+ if (ctx) {
7344
+ const stripeColor1 = themeMode === 'light' ? 'rgba(0,0,0,0)' : 'rgba(0,0,0,0.75)';
7345
+ const stripeColor2 = themeMode === 'light' ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.50)';
7346
+ const gradient = direction === 'forward'
7347
+ ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
7348
+ : ctx.createLinearGradient(0, 0, canvasSize, canvasSize);
7349
+ gradient.addColorStop(0, stripeColor1);
7350
+ gradient.addColorStop(0.25, stripeColor1);
7351
+ gradient.addColorStop(0.25, stripeColor2);
7352
+ gradient.addColorStop(0.5, stripeColor2);
7353
+ gradient.addColorStop(0.5, stripeColor1);
7354
+ gradient.addColorStop(0.75, stripeColor1);
7355
+ gradient.addColorStop(0.75, stripeColor2);
7356
+ gradient.addColorStop(1, stripeColor2);
7357
+ ctx.fillStyle = gradient;
7358
+ ctx.fillRect(0, 0, 10, 10);
7359
+ if (direction === 'forward') {
7360
+ if (themeMode === 'light') {
7361
+ forwardFillLight = ctx.createPattern(canvas, 'repeat');
7362
+ }
7363
+ else {
7364
+ forwardFillDark = ctx.createPattern(canvas, 'repeat');
7365
+ }
7366
+ }
7367
+ else {
7368
+ if (themeMode === 'light') {
7369
+ backwardFillLight = ctx.createPattern(canvas, 'repeat');
7370
+ }
7371
+ else {
7372
+ backwardFillDark = ctx.createPattern(canvas, 'repeat');
7373
+ }
7374
+ }
7564
7375
  }
7565
7376
  }
7566
7377
  }
@@ -7573,12 +7384,25 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7573
7384
  const rowHeight = apolloRowHeight;
7574
7385
  const exonHeight = Math.round(0.6 * rowHeight);
7575
7386
  const cdsHeight = Math.round(0.9 * rowHeight);
7576
- const { strand } = feature;
7577
- const { children } = feature;
7387
+ const { children, min, strand } = feature;
7578
7388
  if (!children) {
7579
7389
  return;
7580
7390
  }
7581
7391
  const { apolloSelectedFeature } = session;
7392
+ // Draw background for gene
7393
+ const topLevelFeatureMinX = (lgv.bpToPx({
7394
+ refName,
7395
+ coord: min,
7396
+ regionNumber: displayedRegionIndex,
7397
+ })?.offsetPx ?? 0) - offsetPx;
7398
+ const topLevelFeatureWidthPx = feature.length / bpPerPx;
7399
+ const topLevelFeatureStartPx = reversed
7400
+ ? topLevelFeatureMinX - topLevelFeatureWidthPx
7401
+ : topLevelFeatureMinX;
7402
+ const topLevelFeatureTop = row * rowHeight;
7403
+ const topLevelFeatureHeight = getRowCount$1(feature) * rowHeight;
7404
+ ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6);
7405
+ ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
7582
7406
  // Draw lines on different rows for each mRNA
7583
7407
  let currentRow = 0;
7584
7408
  for (const [, mrna] of children) {
@@ -7610,6 +7434,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7610
7434
  currentRow += 1;
7611
7435
  }
7612
7436
  }
7437
+ const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
7438
+ const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
7613
7439
  // Draw exon and CDS for each mRNA
7614
7440
  currentRow = 0;
7615
7441
  for (const [, child] of children) {
@@ -7746,11 +7572,31 @@ function drawHover$1(stateModel, ctx) {
7746
7572
  ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature));
7747
7573
  }
7748
7574
  function getFeatureFromLayout$1(feature, bp, row) {
7749
- const featureInThisRow = featuresForRow$1(feature)[row];
7575
+ const featureInThisRow = featuresForRow$1(feature)[row] || [];
7750
7576
  for (const f of featureInThisRow) {
7577
+ let featureObj;
7751
7578
  if (bp >= f.min && bp <= f.max && f.parent) {
7752
- return f;
7579
+ featureObj = f;
7580
+ }
7581
+ if (!featureObj) {
7582
+ continue;
7583
+ }
7584
+ if (featureObj.type === 'CDS' &&
7585
+ featureObj.parent &&
7586
+ featureObj.parent.type === 'mRNA') {
7587
+ const { cdsLocations } = featureObj.parent;
7588
+ for (const cdsLoc of cdsLocations) {
7589
+ for (const loc of cdsLoc) {
7590
+ if (bp >= loc.min && bp <= loc.max) {
7591
+ return featureObj;
7592
+ }
7593
+ }
7594
+ }
7595
+ // If mouse position is in the intron region, return the mRNA
7596
+ return featureObj.parent;
7753
7597
  }
7598
+ // If mouse position is in a feature that is not a CDS, return the feature
7599
+ return featureObj;
7754
7600
  }
7755
7601
  return feature;
7756
7602
  }
@@ -8213,6 +8059,7 @@ async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
8213
8059
  return;
8214
8060
  }
8215
8061
 
8062
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
8216
8063
  const useStyles$3 = makeStyles()((theme) => ({
8217
8064
  scrollableTable: {
8218
8065
  width: '100%',
@@ -8385,7 +8232,7 @@ function stateModelFactory$1(pluginManager, configSchema) {
8385
8232
  .named('LinearApolloDisplay');
8386
8233
  }
8387
8234
 
8388
- /* eslint-disable @typescript-eslint/unbound-method */
8235
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
8389
8236
  const useStyles$1 = makeStyles()((theme) => ({
8390
8237
  canvasContainer: {
8391
8238
  position: 'relative',
@@ -8552,6 +8399,11 @@ const useStyles = makeStyles()((theme) => ({
8552
8399
  // position: 'relative',
8553
8400
  userSelect: 'none',
8554
8401
  },
8402
+ alertContainer: {
8403
+ display: 'flex',
8404
+ alignItems: 'center',
8405
+ justifyContent: 'center',
8406
+ },
8555
8407
  }));
8556
8408
  function scrollSelectedFeatureIntoView(model, scrollContainerRef) {
8557
8409
  const { apolloRowHeight, selectedFeature } = model;
@@ -8574,18 +8426,18 @@ const ResizeHandle = ({ onResize, }) => {
8574
8426
  const cancelDrag = useCallback((event) => {
8575
8427
  event.stopPropagation();
8576
8428
  event.preventDefault();
8577
- window.removeEventListener('mousemove', mouseMove);
8578
- window.removeEventListener('mouseup', cancelDrag);
8579
- window.removeEventListener('mouseleave', cancelDrag);
8429
+ globalThis.removeEventListener('mousemove', mouseMove);
8430
+ globalThis.removeEventListener('mouseup', cancelDrag);
8431
+ globalThis.removeEventListener('mouseleave', cancelDrag);
8580
8432
  }, [mouseMove]);
8581
8433
  return (
8582
8434
  // TODO: a11y
8583
8435
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8584
8436
  React__default.createElement("div", { onMouseDown: (event) => {
8585
8437
  event.stopPropagation();
8586
- window.addEventListener('mousemove', mouseMove);
8587
- window.addEventListener('mouseup', cancelDrag);
8588
- window.addEventListener('mouseleave', cancelDrag);
8438
+ globalThis.addEventListener('mousemove', mouseMove);
8439
+ globalThis.addEventListener('mouseup', cancelDrag);
8440
+ globalThis.addEventListener('mouseleave', cancelDrag);
8589
8441
  }, onClick: (e) => {
8590
8442
  e.stopPropagation();
8591
8443
  e.preventDefault();
@@ -8600,6 +8452,10 @@ const AccordionControl = observer(function AccordionControl({ onClick, onResize,
8600
8452
  title ? (React__default.createElement(Typography, { className: classes.title, variant: "caption", component: "span" }, title)) : null)));
8601
8453
  });
8602
8454
  const DisplayComponent = observer(function DisplayComponent({ model, ...other }) {
8455
+ const session = getSession(model);
8456
+ const { ontologyManager } = session.apolloDataStore;
8457
+ const { featureTypeOntology } = ontologyManager;
8458
+ const ontologyStore = featureTypeOntology?.dataStore;
8603
8459
  const { classes } = useStyles();
8604
8460
  const { detailsHeight, graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
8605
8461
  const canvasScrollContainerRef = useRef(null);
@@ -8609,6 +8465,10 @@ const DisplayComponent = observer(function DisplayComponent({ model, ...other })
8609
8465
  const onDetailsResize = (delta) => {
8610
8466
  model.setDetailsHeight(detailsHeight - delta);
8611
8467
  };
8468
+ if (!ontologyStore) {
8469
+ return (React__default.createElement("div", { className: classes.alertContainer },
8470
+ React__default.createElement(Alert, { severity: "error" }, "Could not load feature type ontology.")));
8471
+ }
8612
8472
  if (graphical && table) {
8613
8473
  const tabularHeight = tabularEditor.isShown ? detailsHeight : 0;
8614
8474
  const featureAreaHeight = isShown
@@ -9178,7 +9038,7 @@ class DesktopFileDriver extends BackendDriver {
9178
9038
  throw new Error(`Assembly ${assemblyName} not found`);
9179
9039
  }
9180
9040
  const { file } = getConf(assembly, ['sequence', 'metadata']);
9181
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9041
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9182
9042
  const fs = require('node:fs');
9183
9043
  const fileContents = await fs.promises.readFile(file, 'utf8');
9184
9044
  return loadAssemblyIntoClient(assemblyName, fileContents, this.clientStore);
@@ -9288,7 +9148,7 @@ class DesktopFileDriver extends BackendDriver {
9288
9148
  });
9289
9149
  }
9290
9150
  const gff3Contents = gff.formatSync(gff3Items);
9291
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9151
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9292
9152
  const fs = require('node:fs');
9293
9153
  await fs.promises.writeFile(file, gff3Contents, 'utf8');
9294
9154
  const results = new ValidationResultSet();
@@ -9305,6 +9165,7 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9305
9165
  typeName: types.optional(types.literal('Client'), 'Client'),
9306
9166
  assemblies: types.map(ApolloAssembly),
9307
9167
  checkResults: types.map(CheckResult),
9168
+ ontologyManager: types.optional(OntologyManagerType, {}),
9308
9169
  })
9309
9170
  .views((self) => ({
9310
9171
  get internetAccounts() {
@@ -9378,7 +9239,6 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9378
9239
  desktopFileDriver: isElectron
9379
9240
  ? new DesktopFileDriver(self)
9380
9241
  : undefined,
9381
- ontologyManager: OntologyManagerType.create(),
9382
9242
  }))
9383
9243
  .actions((self) => ({
9384
9244
  afterCreate() {
@@ -9831,6 +9691,7 @@ function extendSession(pluginManager, sessionModel) {
9831
9691
  postProcessor(snap) {
9832
9692
  snap.apolloSelectedFeature = undefined;
9833
9693
  const assemblies = Object.fromEntries(Object.entries(snap.apolloDataStore.assemblies).filter(([, assembly]) => assembly.backendDriverType === 'InMemoryFileDriver'));
9694
+ // @ts-expect-error ontologyManager isn't actually required
9834
9695
  snap.apolloDataStore = {
9835
9696
  typeName: 'Client',
9836
9697
  assemblies,
@@ -10265,7 +10126,7 @@ class RefNameAliasAdapter extends BaseAdapter {
10265
10126
  this.refNameAliases = refNameAliases;
10266
10127
  return refNameAliases;
10267
10128
  }
10268
- async freeResources() {
10129
+ freeResources() {
10269
10130
  // no resources to free
10270
10131
  }
10271
10132
  }