@apollo-annotation/jbrowse-plugin-apollo 0.1.20 → 0.2.0

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 (61) hide show
  1. package/dist/index.esm.js +552 -642
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +560 -650
  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 +11294 -1232
  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 -4
  25. package/src/LinearApolloDisplay/configSchema.ts +5 -14
  26. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +25 -3
  27. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +95 -32
  28. package/src/LinearApolloDisplay/index.ts +1 -1
  29. package/src/LinearApolloDisplay/stateModel/base.ts +104 -17
  30. package/src/LinearApolloDisplay/stateModel/index.ts +1 -1
  31. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +1 -1
  32. package/src/LinearApolloDisplay/stateModel/rendering.ts +1 -1
  33. package/src/OntologyManager/OntologyStore/fulltext.ts +5 -2
  34. package/src/OntologyManager/OntologyStore/index.ts +25 -22
  35. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +8 -3
  36. package/src/OntologyManager/index.ts +31 -8
  37. package/src/SixFrameFeatureDisplay/stateModel.ts +1 -1
  38. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +1 -0
  39. package/src/TabularEditor/HybridGrid/NumberCell.tsx +1 -0
  40. package/src/TabularEditor/model.ts +1 -1
  41. package/src/components/AddChildFeature.tsx +1 -0
  42. package/src/components/AddFeature.tsx +1 -1
  43. package/src/components/AddRefSeqAliases.tsx +1 -0
  44. package/src/components/CopyFeature.tsx +1 -0
  45. package/src/components/DeleteAssembly.tsx +1 -0
  46. package/src/components/DeleteFeature.tsx +1 -0
  47. package/src/components/LogOut.tsx +2 -1
  48. package/src/components/ManageChecks.tsx +1 -1
  49. package/src/components/ManageUsers.tsx +2 -1
  50. package/src/components/OntologyTermAutocomplete.tsx +7 -9
  51. package/src/components/OntologyTermMultiSelect.tsx +2 -1
  52. package/src/components/OpenLocalFile.tsx +3 -1
  53. package/src/components/ViewChangeLog.tsx +1 -0
  54. package/src/components/ViewCheckResults.tsx +1 -0
  55. package/src/config.ts +5 -0
  56. package/src/extensions/annotationFromPileup.ts +1 -1
  57. package/src/index.ts +2 -2
  58. package/src/makeDisplayComponent.tsx +77 -32
  59. package/src/session/ClientDataStore.ts +1 -1
  60. package/src/session/session.ts +2 -1
  61. package/src/LinearApolloDisplay/stateModel/trackHeightMixin.ts +0 -43
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.20";
53
+ var version = "0.2.0";
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
  },
@@ -4056,7 +4066,7 @@ class ApolloSequenceAdapter extends BaseSequenceAdapter {
4056
4066
  freeResources( /* { region } */) { }
4057
4067
  }
4058
4068
 
4059
- var configSchema$2 = ConfigurationSchema('ApolloSequenceAdapter', {
4069
+ var configSchema$3 = ConfigurationSchema('ApolloSequenceAdapter', {
4060
4070
  assemblyId: {
4061
4071
  type: 'string',
4062
4072
  defaultValue: '',
@@ -4066,7 +4076,7 @@ var configSchema$2 = ConfigurationSchema('ApolloSequenceAdapter', {
4066
4076
  function installApolloSequenceAdapter(pluginManager) {
4067
4077
  pluginManager.addAdapterType(() => new AdapterType({
4068
4078
  name: 'ApolloSequenceAdapter',
4069
- configSchema: configSchema$2,
4079
+ configSchema: configSchema$3,
4070
4080
  adapterMetadata: {
4071
4081
  category: undefined,
4072
4082
  hiddenFromGUI: true,
@@ -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,
@@ -4660,7 +4665,7 @@ class ApolloTextSearchAdapter extends BaseAdapter {
4660
4665
  freeResources() { }
4661
4666
  }
4662
4667
 
4663
- var configSchema$1 = ConfigurationSchema('ApolloTextSearchAdapter', {
4668
+ var configSchema$2 = ConfigurationSchema('ApolloTextSearchAdapter', {
4664
4669
  assemblyNames: {
4665
4670
  type: 'stringArray',
4666
4671
  defaultValue: [],
@@ -4683,7 +4688,7 @@ function installApolloTextSearchAdapter(pluginManager) {
4683
4688
  pluginManager.addTextSearchAdapterType(() => new TextSearchAdapterType({
4684
4689
  name: 'ApolloTextSearchAdapter',
4685
4690
  displayName: 'Apollo text search adapter',
4686
- configSchema: configSchema$1,
4691
+ configSchema: configSchema$2,
4687
4692
  AdapterClass: ApolloTextSearchAdapter,
4688
4693
  description: 'Apollo Text Search adapter',
4689
4694
  }));
@@ -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) => ({
@@ -5986,11 +5767,7 @@ const ApolloTranscriptDetailsWidget = observer(function ApolloTranscriptDetails(
5986
5767
  React__default.createElement(TranscriptSequence, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName })));
5987
5768
  });
5988
5769
 
5989
- function configSchemaFactory$1(pluginManager) {
5990
- const LGVPlugin = pluginManager.getPlugin('LinearGenomeViewPlugin');
5991
- const { baseLinearDisplayConfigSchema } = LGVPlugin.exports;
5992
- return ConfigurationSchema('LinearApolloDisplay', { height: { type: 'number', defaultValue: 500 } }, { baseConfiguration: baseLinearDisplayConfigSchema, explicitlyTyped: true });
5993
- }
5770
+ const configSchema$1 = ConfigurationSchema('LinearApolloDisplay', {}, { explicitIdentifier: 'displayId', explicitlyTyped: true });
5994
5771
 
5995
5772
  function handleFeatureTypeChange(changeManager, feature, oldType, newType) {
5996
5773
  const featureId = feature._id;
@@ -6180,7 +5957,7 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
6180
5957
  return menuItems;
6181
5958
  }
6182
5959
 
6183
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
5960
+ /* eslint-disable @typescript-eslint/unbound-method */
6184
5961
  const useStyles$5 = makeStyles()((theme) => ({
6185
5962
  inputWrapper: {
6186
5963
  position: 'relative',
@@ -6234,56 +6011,16 @@ const NumberCell = observer(function NumberCell({ initialValue, notifyError, onC
6234
6011
  } })));
6235
6012
  });
6236
6013
 
6237
- // TODO: get this added to LGV runtime exports so we don't have to duplicate it
6238
6014
  const minDisplayHeight = 20;
6239
- /**
6240
- * #stateModel TrackHeightMixin
6241
- * #category display
6242
- */
6243
- const TrackHeightMixin = types
6244
- .model({
6245
- heightPreConfig: types.maybe(types.refinement('displayHeight', types.number, (n) => n >= minDisplayHeight)),
6246
- })
6247
- .volatile(() => ({
6248
- scrollTop: 0,
6249
- }))
6250
- .views((self) => ({
6251
- get height() {
6252
- // @ts-expect-error getConf needs self.configuration
6253
- return self.heightPreConfig ?? getConf(self, 'height');
6254
- },
6255
- }))
6256
- .actions((self) => ({
6257
- setScrollTop(scrollTop) {
6258
- self.scrollTop = scrollTop;
6259
- },
6260
- setHeight(displayHeight) {
6261
- self.heightPreConfig = Math.max(displayHeight, minDisplayHeight);
6262
- return self.height;
6263
- },
6264
- resizeHeight(distance) {
6265
- const oldHeight = self.height;
6266
- const newHeight = this.setHeight(self.height + distance);
6267
- return newHeight - oldHeight;
6268
- },
6269
- }));
6270
-
6271
6015
  function baseModelFactory(_pluginManager, configSchema) {
6272
- // TODO: Restore this when TRackHeightMixin is in LGV runtime exports
6273
- // const LGVPlugin = pluginManager.getPlugin(
6274
- // 'LinearGenomeViewPlugin',
6275
- // ) as LinearGenomeViewPlugin
6276
- // const { TrackHeightMixin } = LGVPlugin.exports
6277
- return types
6278
- .compose(BaseDisplay, TrackHeightMixin)
6279
- .named('BaseLinearApolloDisplay')
6016
+ return BaseDisplay.named('BaseLinearApolloDisplay')
6280
6017
  .props({
6281
6018
  type: types.literal('LinearApolloDisplay'),
6282
6019
  configuration: ConfigurationReference(configSchema),
6020
+ graphical: true,
6021
+ table: false,
6022
+ heightPreConfig: types.maybe(types.refinement('displayHeight', types.number, (n) => n >= minDisplayHeight)),
6283
6023
  })
6284
- .volatile((self) => ({
6285
- lgv: getContainingView(self),
6286
- }))
6287
6024
  .views((self) => {
6288
6025
  const { configuration, renderProps: superRenderProps } = self;
6289
6026
  return {
@@ -6296,6 +6033,26 @@ function baseModelFactory(_pluginManager, configSchema) {
6296
6033
  },
6297
6034
  };
6298
6035
  })
6036
+ .volatile(() => ({
6037
+ scrollTop: 0,
6038
+ }))
6039
+ .views((self) => ({
6040
+ get lgv() {
6041
+ return getContainingView(self);
6042
+ },
6043
+ get height() {
6044
+ if (self.heightPreConfig) {
6045
+ return self.heightPreConfig;
6046
+ }
6047
+ if (self.graphical && self.table) {
6048
+ return 500;
6049
+ }
6050
+ if (self.graphical) {
6051
+ return 200;
6052
+ }
6053
+ return 300;
6054
+ },
6055
+ }))
6299
6056
  .views((self) => ({
6300
6057
  get rendererTypeName() {
6301
6058
  return self.configuration.renderer.type;
@@ -6352,6 +6109,73 @@ function baseModelFactory(_pluginManager, configSchema) {
6352
6109
  .apolloSelectedFeature;
6353
6110
  },
6354
6111
  }))
6112
+ .actions((self) => ({
6113
+ setScrollTop(scrollTop) {
6114
+ self.scrollTop = scrollTop;
6115
+ },
6116
+ setHeight(displayHeight) {
6117
+ self.heightPreConfig = Math.max(displayHeight, minDisplayHeight);
6118
+ return self.height;
6119
+ },
6120
+ resizeHeight(distance) {
6121
+ const oldHeight = self.height;
6122
+ const newHeight = this.setHeight(self.height + distance);
6123
+ return newHeight - oldHeight;
6124
+ },
6125
+ showGraphicalOnly() {
6126
+ self.graphical = true;
6127
+ self.table = false;
6128
+ },
6129
+ showTableOnly() {
6130
+ self.graphical = false;
6131
+ self.table = true;
6132
+ },
6133
+ showGraphicalAndTable() {
6134
+ self.graphical = true;
6135
+ self.table = true;
6136
+ },
6137
+ }))
6138
+ .views((self) => {
6139
+ const { trackMenuItems: superTrackMenuItems } = self;
6140
+ return {
6141
+ trackMenuItems() {
6142
+ const { graphical, table } = self;
6143
+ return [
6144
+ ...superTrackMenuItems(),
6145
+ {
6146
+ type: 'subMenu',
6147
+ label: 'Appearance',
6148
+ subMenu: [
6149
+ {
6150
+ label: 'Show graphical display',
6151
+ type: 'radio',
6152
+ checked: graphical && !table,
6153
+ onClick: () => {
6154
+ self.showGraphicalOnly();
6155
+ },
6156
+ },
6157
+ {
6158
+ label: 'Show table display',
6159
+ type: 'radio',
6160
+ checked: table && !graphical,
6161
+ onClick: () => {
6162
+ self.showTableOnly();
6163
+ },
6164
+ },
6165
+ {
6166
+ label: 'Show both graphical and table display',
6167
+ type: 'radio',
6168
+ checked: table && graphical,
6169
+ onClick: () => {
6170
+ self.showGraphicalAndTable();
6171
+ },
6172
+ },
6173
+ ],
6174
+ },
6175
+ ];
6176
+ },
6177
+ };
6178
+ })
6355
6179
  .actions((self) => ({
6356
6180
  setSelectedFeature(feature) {
6357
6181
  self.session.apolloSetSelectedFeature(feature);
@@ -6362,7 +6186,9 @@ function baseModelFactory(_pluginManager, configSchema) {
6362
6186
  return;
6363
6187
  }
6364
6188
  void self.session.apolloDataStore.loadFeatures(self.regions);
6365
- void self.session.apolloDataStore.loadRefSeq(self.regions);
6189
+ if (self.lgv.bpPerPx <= 3) {
6190
+ void self.session.apolloDataStore.loadRefSeq(self.regions);
6191
+ }
6366
6192
  }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
6367
6193
  },
6368
6194
  }));
@@ -7247,7 +7073,7 @@ function drawTooltip$2(display, context) {
7247
7073
  if (!position) {
7248
7074
  return;
7249
7075
  }
7250
- const { layoutIndex, layoutRow } = position;
7076
+ const { featureRow, layoutIndex, layoutRow } = position;
7251
7077
  const { bpPerPx, displayedRegions, offsetPx } = lgv;
7252
7078
  const displayedRegion = displayedRegions[layoutIndex];
7253
7079
  const { refName, reversed } = displayedRegion;
@@ -7259,7 +7085,7 @@ function drawTooltip$2(display, context) {
7259
7085
  coord: reversed ? max : min,
7260
7086
  regionNumber: layoutIndex,
7261
7087
  })?.offsetPx ?? 0) - offsetPx;
7262
- const top = layoutRow * apolloRowHeight;
7088
+ const top = (layoutRow + featureRow) * apolloRowHeight;
7263
7089
  const widthPx = length / bpPerPx;
7264
7090
  const featureType = `Type: ${feature.type}`;
7265
7091
  const { attributes } = feature;
@@ -7405,6 +7231,20 @@ function getContextMenuItems$2(display) {
7405
7231
  session.showWidget(apolloFeatureWidget);
7406
7232
  },
7407
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
+ }
7408
7248
  return menuItems;
7409
7249
  }
7410
7250
  function getFeatureFromLayout$2(feature, _bp, _row) {
@@ -7489,35 +7329,49 @@ const boxGlyph = {
7489
7329
  onMouseUp: onMouseUp$2,
7490
7330
  };
7491
7331
 
7492
- let forwardFill = null;
7493
- let backwardFill = null;
7494
- 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) {
7495
7337
  for (const direction of ['forward', 'backward']) {
7496
- const canvas = document.createElement('canvas');
7497
- const canvasSize = 10;
7498
- canvas.width = canvas.height = canvasSize;
7499
- const ctx = canvas.getContext('2d');
7500
- if (ctx) {
7501
- const stripeColor1 = 'rgba(0,0,0,0)';
7502
- const stripeColor2 = 'rgba(255,255,255,0.25)';
7503
- const gradient = direction === 'forward'
7504
- ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
7505
- : ctx.createLinearGradient(0, 0, canvasSize, canvasSize);
7506
- gradient.addColorStop(0, stripeColor1);
7507
- gradient.addColorStop(0.25, stripeColor1);
7508
- gradient.addColorStop(0.25, stripeColor2);
7509
- gradient.addColorStop(0.5, stripeColor2);
7510
- gradient.addColorStop(0.5, stripeColor1);
7511
- gradient.addColorStop(0.75, stripeColor1);
7512
- gradient.addColorStop(0.75, stripeColor2);
7513
- gradient.addColorStop(1, stripeColor2);
7514
- ctx.fillStyle = gradient;
7515
- ctx.fillRect(0, 0, 10, 10);
7516
- if (direction === 'forward') {
7517
- forwardFill = ctx.createPattern(canvas, 'repeat');
7518
- }
7519
- else {
7520
- 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
+ }
7521
7375
  }
7522
7376
  }
7523
7377
  }
@@ -7530,12 +7384,25 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7530
7384
  const rowHeight = apolloRowHeight;
7531
7385
  const exonHeight = Math.round(0.6 * rowHeight);
7532
7386
  const cdsHeight = Math.round(0.9 * rowHeight);
7533
- const { strand } = feature;
7534
- const { children } = feature;
7387
+ const { children, min, strand } = feature;
7535
7388
  if (!children) {
7536
7389
  return;
7537
7390
  }
7538
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);
7539
7406
  // Draw lines on different rows for each mRNA
7540
7407
  let currentRow = 0;
7541
7408
  for (const [, mrna] of children) {
@@ -7567,6 +7434,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7567
7434
  currentRow += 1;
7568
7435
  }
7569
7436
  }
7437
+ const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
7438
+ const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
7570
7439
  // Draw exon and CDS for each mRNA
7571
7440
  currentRow = 0;
7572
7441
  for (const [, child] of children) {
@@ -7703,11 +7572,31 @@ function drawHover$1(stateModel, ctx) {
7703
7572
  ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature));
7704
7573
  }
7705
7574
  function getFeatureFromLayout$1(feature, bp, row) {
7706
- const featureInThisRow = featuresForRow$1(feature)[row];
7575
+ const featureInThisRow = featuresForRow$1(feature)[row] || [];
7707
7576
  for (const f of featureInThisRow) {
7577
+ let featureObj;
7708
7578
  if (bp >= f.min && bp <= f.max && f.parent) {
7709
- return f;
7579
+ featureObj = f;
7580
+ }
7581
+ if (!featureObj) {
7582
+ continue;
7710
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;
7597
+ }
7598
+ // If mouse position is in a feature that is not a CDS, return the feature
7599
+ return featureObj;
7711
7600
  }
7712
7601
  return feature;
7713
7602
  }
@@ -8170,6 +8059,7 @@ async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
8170
8059
  return;
8171
8060
  }
8172
8061
 
8062
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
8173
8063
  const useStyles$3 = makeStyles()((theme) => ({
8174
8064
  scrollableTable: {
8175
8065
  width: '100%',
@@ -8342,7 +8232,7 @@ function stateModelFactory$1(pluginManager, configSchema) {
8342
8232
  .named('LinearApolloDisplay');
8343
8233
  }
8344
8234
 
8345
- /* eslint-disable @typescript-eslint/unbound-method */
8235
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
8346
8236
  const useStyles$1 = makeStyles()((theme) => ({
8347
8237
  canvasContainer: {
8348
8238
  position: 'relative',
@@ -8365,7 +8255,7 @@ const useStyles$1 = makeStyles()((theme) => ({
8365
8255
  const LinearApolloDisplay = observer(function LinearApolloDisplay(props) {
8366
8256
  const theme = useTheme();
8367
8257
  const { model } = props;
8368
- const { apolloRowHeight, contextMenuItems: getContextMenuItems, cursor, featuresHeight, isShown, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, regionCannotBeRendered, session, setCanvas, setCollaboratorCanvas, setOverlayCanvas, setSeqTrackCanvas, setSeqTrackOverlayCanvas, setTheme, tabularEditor, } = model;
8258
+ const { apolloRowHeight, contextMenuItems: getContextMenuItems, cursor, featuresHeight, isShown, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, regionCannotBeRendered, session, setCanvas, setCollaboratorCanvas, setOverlayCanvas, setSeqTrackCanvas, setSeqTrackOverlayCanvas, setTheme, } = model;
8369
8259
  const { classes } = useStyles$1();
8370
8260
  const lgv = getContainingView(model);
8371
8261
  useEffect(() => {
@@ -8422,9 +8312,7 @@ const LinearApolloDisplay = observer(function LinearApolloDisplay(props) {
8422
8312
  React__default.createElement("canvas", { ref: async (node) => {
8423
8313
  await Promise.resolve();
8424
8314
  setOverlayCanvas(node);
8425
- }, width: lgv.dynamicBlocks.totalWidthPx, height: featuresHeight, onMouseMove: onMouseMove, onMouseLeave: onMouseLeave, onMouseDown: onMouseDown, onMouseUp: onMouseUp, onClick: () => {
8426
- tabularEditor.showPane();
8427
- }, className: classes.canvas, style: { cursor: cursor ?? 'default' }, "data-testid": "overlayCanvas" }),
8315
+ }, width: lgv.dynamicBlocks.totalWidthPx, height: featuresHeight, onMouseMove: onMouseMove, onMouseLeave: onMouseLeave, onMouseDown: onMouseDown, onMouseUp: onMouseUp, className: classes.canvas, style: { cursor: cursor ?? 'default' }, "data-testid": "overlayCanvas" }),
8428
8316
  lgv.displayedRegions.flatMap((region, idx) => {
8429
8317
  const assembly = assemblyManager.get(region.assemblyName);
8430
8318
  return [...session.apolloDataStore.checkResults.values()]
@@ -8511,6 +8399,11 @@ const useStyles = makeStyles()((theme) => ({
8511
8399
  // position: 'relative',
8512
8400
  userSelect: 'none',
8513
8401
  },
8402
+ alertContainer: {
8403
+ display: 'flex',
8404
+ alignItems: 'center',
8405
+ justifyContent: 'center',
8406
+ },
8514
8407
  }));
8515
8408
  function scrollSelectedFeatureIntoView(model, scrollContainerRef) {
8516
8409
  const { apolloRowHeight, selectedFeature } = model;
@@ -8533,18 +8426,18 @@ const ResizeHandle = ({ onResize, }) => {
8533
8426
  const cancelDrag = useCallback((event) => {
8534
8427
  event.stopPropagation();
8535
8428
  event.preventDefault();
8536
- window.removeEventListener('mousemove', mouseMove);
8537
- window.removeEventListener('mouseup', cancelDrag);
8538
- window.removeEventListener('mouseleave', cancelDrag);
8429
+ globalThis.removeEventListener('mousemove', mouseMove);
8430
+ globalThis.removeEventListener('mouseup', cancelDrag);
8431
+ globalThis.removeEventListener('mouseleave', cancelDrag);
8539
8432
  }, [mouseMove]);
8540
8433
  return (
8541
8434
  // TODO: a11y
8542
8435
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8543
8436
  React__default.createElement("div", { onMouseDown: (event) => {
8544
8437
  event.stopPropagation();
8545
- window.addEventListener('mousemove', mouseMove);
8546
- window.addEventListener('mouseup', cancelDrag);
8547
- window.addEventListener('mouseleave', cancelDrag);
8438
+ globalThis.addEventListener('mousemove', mouseMove);
8439
+ globalThis.addEventListener('mouseup', cancelDrag);
8440
+ globalThis.addEventListener('mouseleave', cancelDrag);
8548
8441
  }, onClick: (e) => {
8549
8442
  e.stopPropagation();
8550
8443
  e.preventDefault();
@@ -8559,26 +8452,42 @@ const AccordionControl = observer(function AccordionControl({ onClick, onResize,
8559
8452
  title ? (React__default.createElement(Typography, { className: classes.title, variant: "caption", component: "span" }, title)) : null)));
8560
8453
  });
8561
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;
8562
8459
  const { classes } = useStyles();
8563
- const { height: overallHeight, isShown, selectedFeature, tabularEditor, toggleShown, } = model;
8564
- const detailsHeight = tabularEditor.isShown ? model.detailsHeight : 0;
8565
- const featureAreaHeight = isShown
8566
- ? overallHeight - detailsHeight - accordionControlHeight * 2
8567
- : 0;
8568
- const onDetailsResize = (delta) => {
8569
- model.setDetailsHeight(model.detailsHeight - delta);
8570
- };
8460
+ const { detailsHeight, graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
8571
8461
  const canvasScrollContainerRef = useRef(null);
8572
8462
  useEffect(() => {
8573
8463
  scrollSelectedFeatureIntoView(model, canvasScrollContainerRef);
8574
8464
  }, [model, selectedFeature]);
8465
+ const onDetailsResize = (delta) => {
8466
+ model.setDetailsHeight(detailsHeight - delta);
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
+ }
8472
+ if (graphical && table) {
8473
+ const tabularHeight = tabularEditor.isShown ? detailsHeight : 0;
8474
+ const featureAreaHeight = isShown
8475
+ ? overallHeight - detailsHeight - accordionControlHeight * 2
8476
+ : 0;
8477
+ return (React__default.createElement("div", { style: { height: overallHeight } },
8478
+ React__default.createElement(AccordionControl, { open: isShown, title: "Graphical", onClick: toggleShown }),
8479
+ React__default.createElement("div", { className: classes.shading, ref: canvasScrollContainerRef, style: { height: featureAreaHeight } },
8480
+ React__default.createElement(LinearApolloDisplay, { model: model, ...other })),
8481
+ React__default.createElement(AccordionControl, { title: "Table", open: tabularEditor.isShown, onClick: tabularEditor.togglePane, onResize: onDetailsResize }),
8482
+ React__default.createElement("div", { className: classes.details, style: { height: tabularHeight } },
8483
+ React__default.createElement(TabularEditorPane, { model: model }))));
8484
+ }
8485
+ if (graphical) {
8486
+ return (React__default.createElement("div", { className: classes.shading, ref: canvasScrollContainerRef, style: { height: overallHeight } },
8487
+ React__default.createElement(LinearApolloDisplay, { model: model, ...other })));
8488
+ }
8575
8489
  return (React__default.createElement("div", { className: classes.details, style: { height: overallHeight } },
8576
- React__default.createElement(AccordionControl, { open: isShown, title: "Graphical", onClick: toggleShown }),
8577
- React__default.createElement("div", { className: classes.shading, ref: canvasScrollContainerRef, style: { height: featureAreaHeight } },
8578
- React__default.createElement(LinearApolloDisplay, { model: model, ...other })),
8579
- React__default.createElement(AccordionControl, { title: "Table", open: tabularEditor.isShown, onClick: tabularEditor.togglePane, onResize: onDetailsResize }),
8580
- React__default.createElement("div", { style: { height: detailsHeight } },
8581
- React__default.createElement(TabularEditorPane, { model: model }))));
8490
+ React__default.createElement(TabularEditorPane, { model: model })));
8582
8491
  });
8583
8492
  function makeSixFrameDisplayComponent(pluginManager) {
8584
8493
  const LGVPlugin = pluginManager.getPlugin('LinearGenomeViewPlugin');
@@ -9129,7 +9038,7 @@ class DesktopFileDriver extends BackendDriver {
9129
9038
  throw new Error(`Assembly ${assemblyName} not found`);
9130
9039
  }
9131
9040
  const { file } = getConf(assembly, ['sequence', 'metadata']);
9132
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9041
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9133
9042
  const fs = require('node:fs');
9134
9043
  const fileContents = await fs.promises.readFile(file, 'utf8');
9135
9044
  return loadAssemblyIntoClient(assemblyName, fileContents, this.clientStore);
@@ -9239,7 +9148,7 @@ class DesktopFileDriver extends BackendDriver {
9239
9148
  });
9240
9149
  }
9241
9150
  const gff3Contents = gff.formatSync(gff3Items);
9242
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9151
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9243
9152
  const fs = require('node:fs');
9244
9153
  await fs.promises.writeFile(file, gff3Contents, 'utf8');
9245
9154
  const results = new ValidationResultSet();
@@ -9256,6 +9165,7 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9256
9165
  typeName: types.optional(types.literal('Client'), 'Client'),
9257
9166
  assemblies: types.map(ApolloAssembly),
9258
9167
  checkResults: types.map(CheckResult),
9168
+ ontologyManager: types.optional(OntologyManagerType, {}),
9259
9169
  })
9260
9170
  .views((self) => ({
9261
9171
  get internetAccounts() {
@@ -9329,7 +9239,6 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9329
9239
  desktopFileDriver: isElectron
9330
9240
  ? new DesktopFileDriver(self)
9331
9241
  : undefined,
9332
- ontologyManager: OntologyManagerType.create(),
9333
9242
  }))
9334
9243
  .actions((self) => ({
9335
9244
  afterCreate() {
@@ -9782,6 +9691,7 @@ function extendSession(pluginManager, sessionModel) {
9782
9691
  postProcessor(snap) {
9783
9692
  snap.apolloSelectedFeature = undefined;
9784
9693
  const assemblies = Object.fromEntries(Object.entries(snap.apolloDataStore.assemblies).filter(([, assembly]) => assembly.backendDriverType === 'InMemoryFileDriver'));
9694
+ // @ts-expect-error ontologyManager isn't actually required
9785
9695
  snap.apolloDataStore = {
9786
9696
  typeName: 'Client',
9787
9697
  assemblies,
@@ -10216,7 +10126,7 @@ class RefNameAliasAdapter extends BaseAdapter {
10216
10126
  this.refNameAliases = refNameAliases;
10217
10127
  return refNameAliases;
10218
10128
  }
10219
- async freeResources() {
10129
+ freeResources() {
10220
10130
  // no resources to free
10221
10131
  }
10222
10132
  }
@@ -10298,7 +10208,7 @@ class ApolloPlugin extends Plugin {
10298
10208
  });
10299
10209
  });
10300
10210
  pluginManager.addDisplayType(() => {
10301
- const configSchema = configSchemaFactory$1(pluginManager);
10211
+ const configSchema = configSchema$1;
10302
10212
  return new DisplayType({
10303
10213
  name: 'LinearApolloDisplay',
10304
10214
  configSchema,