@apollo-annotation/jbrowse-plugin-apollo 0.3.4 → 0.3.5

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 (44) hide show
  1. package/dist/index.esm.js +1513 -1074
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +1510 -1065
  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 +4681 -2097
  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 -4
  12. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +13 -0
  13. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +88 -17
  14. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +144 -22
  15. package/src/FeatureDetailsWidget/Attributes.tsx +93 -43
  16. package/src/FeatureDetailsWidget/BasicInformation.tsx +2 -4
  17. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +6 -4
  18. package/src/FeatureDetailsWidget/Sequence.tsx +16 -33
  19. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +137 -92
  20. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +600 -0
  21. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +54 -0
  22. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +13 -6
  23. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +6 -27
  24. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +35 -13
  25. package/src/LinearApolloDisplay/stateModel/layouts.ts +7 -2
  26. package/src/LinearApolloDisplay/stateModel/rendering.ts +4 -0
  27. package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +10 -1
  28. package/src/OntologyManager/OntologyStore/index.test.ts +1 -0
  29. package/src/OntologyManager/index.ts +2 -0
  30. package/src/SixFrameFeatureDisplay/stateModel.ts +5 -1
  31. package/src/TabularEditor/HybridGrid/Feature.tsx +0 -1
  32. package/src/TabularEditor/HybridGrid/NumberCell.tsx +8 -1
  33. package/src/TabularEditor/HybridGrid/ToolBar.tsx +14 -12
  34. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +3 -27
  35. package/src/components/AddAssembly.tsx +608 -291
  36. package/src/components/CreateApolloAnnotation.tsx +144 -37
  37. package/src/components/OntologyTermMultiSelect.tsx +3 -0
  38. package/src/components/index.ts +0 -1
  39. package/src/extensions/annotationFromJBrowseFeature.ts +3 -2
  40. package/src/makeDisplayComponent.tsx +3 -4
  41. package/src/util/annotationFeatureUtils.ts +53 -0
  42. package/src/util/index.ts +1 -0
  43. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +0 -200
  44. package/src/components/ModifyFeatureAttribute.tsx +0 -460
@@ -13,16 +13,17 @@ var mobx = require('mobx');
13
13
  var mobxStateTree = require('mobx-state-tree');
14
14
  var socket_ioClient = require('socket.io-client');
15
15
  var gff = require('@gmod/gff');
16
- var LinkIcon = require('@mui/icons-material/Link');
17
16
  var material = require('@mui/material');
18
- var InputAdornment = require('@mui/material/InputAdornment');
19
- var LinearProgress = require('@mui/material/LinearProgress');
17
+ var mui = require('tss-react/mui');
20
18
  var ObjectID = require('bson-objectid');
21
19
  var React = require('react');
22
20
  var ui = require('@jbrowse/core/ui');
23
21
  var CloseIcon = require('@mui/icons-material/Close');
24
22
  var mobxReact = require('mobx-react');
25
- var mui = require('tss-react/mui');
23
+ var RadioButtonUncheckedIcon = require('@mui/icons-material/RadioButtonUnchecked');
24
+ var RadioButtonCheckedIcon = require('@mui/icons-material/RadioButtonChecked');
25
+ var InfoIcon = require('@mui/icons-material/Info');
26
+ var LinkIcon = require('@mui/icons-material/Link');
26
27
  var mst = require('@jbrowse/core/util/types/mst');
27
28
  var withAsyncIttr = require('idb/with-async-ittr');
28
29
  var aborting = require('@jbrowse/core/util/aborting');
@@ -32,11 +33,9 @@ var equal = require('fast-deep-equal/es6');
32
33
  var fileSaver = require('file-saver');
33
34
  var Checkbox = require('@mui/material/Checkbox');
34
35
  var FormControlLabel = require('@mui/material/FormControlLabel');
36
+ var LinearProgress = require('@mui/material/LinearProgress');
35
37
  var DeleteIcon = require('@mui/icons-material/Delete');
36
38
  var xDataGrid = require('@mui/x-data-grid');
37
- var utils = require('@mui/material/utils');
38
- var highlightMatch = require('autosuggest-highlight/match');
39
- var highlightParse = require('autosuggest-highlight/parse');
40
39
  var nanoid = require('nanoid');
41
40
  var AccountCircleIcon = require('@mui/icons-material/AccountCircle');
42
41
  var AdapterType = require('@jbrowse/core/pluggableElementTypes/AdapterType');
@@ -44,12 +43,19 @@ var BaseAdapter = require('@jbrowse/core/data_adapters/BaseAdapter');
44
43
  var rxjs = require('@jbrowse/core/util/rxjs');
45
44
  var SimpleFeature = require('@jbrowse/core/util/simpleFeature');
46
45
  var BaseResult = require('@jbrowse/core/TextSearch/BaseResults');
46
+ var ExpandMoreIcon = require('@mui/icons-material/ExpandMore');
47
+ var utils = require('@mui/material/utils');
48
+ var highlightMatch = require('autosuggest-highlight/match');
49
+ var highlightParse = require('autosuggest-highlight/parse');
47
50
  var mst$1 = require('@apollo-annotation/mst');
51
+ var styled = require('@emotion/styled');
52
+ var RemoveIcon = require('@mui/icons-material/Remove');
53
+ var ContentCopyIcon = require('@mui/icons-material/ContentCopy');
54
+ var ContentCutIcon = require('@mui/icons-material/ContentCut');
48
55
  var ClearIcon = require('@mui/icons-material/Clear');
49
56
  var UnfoldLessIcon = require('@mui/icons-material/UnfoldLess');
50
57
  var tracks = require('@jbrowse/core/util/tracks');
51
58
  var ExpandLessIcon = require('@mui/icons-material/ExpandLess');
52
- var ExpandMoreIcon = require('@mui/icons-material/ExpandMore');
53
59
  var ErrorIcon = require('@mui/icons-material/Error');
54
60
  var SaveIcon = require('@mui/icons-material/Save');
55
61
 
@@ -76,32 +82,38 @@ function _interopNamespace(e) {
76
82
  var Plugin__default = /*#__PURE__*/_interopDefaultLegacy(Plugin);
77
83
  var AddIcon__default = /*#__PURE__*/_interopDefaultLegacy(AddIcon);
78
84
  var gff__default = /*#__PURE__*/_interopDefaultLegacy(gff);
79
- var LinkIcon__default = /*#__PURE__*/_interopDefaultLegacy(LinkIcon);
80
- var InputAdornment__default = /*#__PURE__*/_interopDefaultLegacy(InputAdornment);
81
- var LinearProgress__default = /*#__PURE__*/_interopDefaultLegacy(LinearProgress);
82
85
  var ObjectID__default = /*#__PURE__*/_interopDefaultLegacy(ObjectID);
83
86
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
84
87
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
85
88
  var CloseIcon__default = /*#__PURE__*/_interopDefaultLegacy(CloseIcon);
89
+ var RadioButtonUncheckedIcon__default = /*#__PURE__*/_interopDefaultLegacy(RadioButtonUncheckedIcon);
90
+ var RadioButtonCheckedIcon__default = /*#__PURE__*/_interopDefaultLegacy(RadioButtonCheckedIcon);
91
+ var InfoIcon__default = /*#__PURE__*/_interopDefaultLegacy(InfoIcon);
92
+ var LinkIcon__default = /*#__PURE__*/_interopDefaultLegacy(LinkIcon);
86
93
  var jsonpath__default = /*#__PURE__*/_interopDefaultLegacy(jsonpath);
87
94
  var equal__default = /*#__PURE__*/_interopDefaultLegacy(equal);
88
95
  var Checkbox__default = /*#__PURE__*/_interopDefaultLegacy(Checkbox);
89
96
  var FormControlLabel__default = /*#__PURE__*/_interopDefaultLegacy(FormControlLabel);
97
+ var LinearProgress__default = /*#__PURE__*/_interopDefaultLegacy(LinearProgress);
90
98
  var DeleteIcon__default = /*#__PURE__*/_interopDefaultLegacy(DeleteIcon);
91
- var highlightMatch__default = /*#__PURE__*/_interopDefaultLegacy(highlightMatch);
92
- var highlightParse__default = /*#__PURE__*/_interopDefaultLegacy(highlightParse);
93
99
  var AccountCircleIcon__default = /*#__PURE__*/_interopDefaultLegacy(AccountCircleIcon);
94
100
  var AdapterType__default = /*#__PURE__*/_interopDefaultLegacy(AdapterType);
95
101
  var SimpleFeature__default = /*#__PURE__*/_interopDefaultLegacy(SimpleFeature);
96
102
  var BaseResult__default = /*#__PURE__*/_interopDefaultLegacy(BaseResult);
103
+ var ExpandMoreIcon__default = /*#__PURE__*/_interopDefaultLegacy(ExpandMoreIcon);
104
+ var highlightMatch__default = /*#__PURE__*/_interopDefaultLegacy(highlightMatch);
105
+ var highlightParse__default = /*#__PURE__*/_interopDefaultLegacy(highlightParse);
106
+ var styled__default = /*#__PURE__*/_interopDefaultLegacy(styled);
107
+ var RemoveIcon__default = /*#__PURE__*/_interopDefaultLegacy(RemoveIcon);
108
+ var ContentCopyIcon__default = /*#__PURE__*/_interopDefaultLegacy(ContentCopyIcon);
109
+ var ContentCutIcon__default = /*#__PURE__*/_interopDefaultLegacy(ContentCutIcon);
97
110
  var ClearIcon__default = /*#__PURE__*/_interopDefaultLegacy(ClearIcon);
98
111
  var UnfoldLessIcon__default = /*#__PURE__*/_interopDefaultLegacy(UnfoldLessIcon);
99
112
  var ExpandLessIcon__default = /*#__PURE__*/_interopDefaultLegacy(ExpandLessIcon);
100
- var ExpandMoreIcon__default = /*#__PURE__*/_interopDefaultLegacy(ExpandMoreIcon);
101
113
  var ErrorIcon__default = /*#__PURE__*/_interopDefaultLegacy(ErrorIcon);
102
114
  var SaveIcon__default = /*#__PURE__*/_interopDefaultLegacy(SaveIcon);
103
115
 
104
- var version = "0.3.4";
116
+ var version = "0.3.5";
105
117
 
106
118
  const ApolloConfigSchema = configuration.ConfigurationSchema('ApolloInternetAccount', {
107
119
  baseURL: {
@@ -181,6 +193,55 @@ async function checkFeatures(assembly) {
181
193
  return checkResults;
182
194
  }
183
195
 
196
+ function getFeatureName(feature) {
197
+ const { attributes } = feature;
198
+ const name = attributes.get('gff_name');
199
+ if (name) {
200
+ return name[0];
201
+ }
202
+ return '';
203
+ }
204
+ function getFeatureId$1(feature) {
205
+ const { attributes } = feature;
206
+ const id = attributes.get('gff_id');
207
+ const transcript_id = attributes.get('transcript_id');
208
+ const exon_id = attributes.get('exon_id');
209
+ const protein_id = attributes.get('protein_id');
210
+ if (id) {
211
+ return id[0];
212
+ }
213
+ if (transcript_id) {
214
+ return transcript_id[0];
215
+ }
216
+ if (exon_id) {
217
+ return exon_id[0];
218
+ }
219
+ if (protein_id) {
220
+ return protein_id[0];
221
+ }
222
+ return '';
223
+ }
224
+ function getFeatureNameOrId$1(feature) {
225
+ const name = getFeatureName(feature);
226
+ const id = getFeatureId$1(feature);
227
+ if (name) {
228
+ return `: ${name}`;
229
+ }
230
+ if (id) {
231
+ return `: ${id}`;
232
+ }
233
+ return '';
234
+ }
235
+ function getStrand(strand) {
236
+ if (strand === 1) {
237
+ return 'Forward';
238
+ }
239
+ if (strand === -1) {
240
+ return 'Reverse';
241
+ }
242
+ return '';
243
+ }
244
+
184
245
  async function createFetchErrorMessage(response, additionalText) {
185
246
  let errorMessage;
186
247
  try {
@@ -221,14 +282,59 @@ const Dialog = mobxReact.observer(function JBrowseDialog(props) {
221
282
  React__default["default"].createElement(CloseIcon__default["default"], null))) }));
222
283
  });
223
284
 
224
- /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
285
+ /* eslint-disable @typescript-eslint/unbound-method */
225
286
  var FileType;
226
287
  (function (FileType) {
227
288
  FileType["GFF3"] = "text/x-gff3";
228
289
  FileType["FASTA"] = "text/x-fasta";
290
+ FileType["BGZIP_FASTA"] = "application/x-bgzip-fasta";
291
+ FileType["FAI"] = "text/x-fai";
292
+ FileType["GZI"] = "application/x-gzi";
229
293
  FileType["EXTERNAL"] = "text/x-external";
230
294
  })(FileType || (FileType = {}));
295
+ const useStyles$e = mui.makeStyles()((theme) => ({
296
+ accordion: {
297
+ border: `1px solid ${theme.palette.divider}`,
298
+ '&:not(:last-child)': {
299
+ borderBottom: 0,
300
+ },
301
+ },
302
+ accordionSummary: {
303
+ flexDirection: 'row-reverse',
304
+ },
305
+ accordionDetails: {
306
+ padding: theme.spacing(2),
307
+ borderTop: '1px solid rgba(0, 0, 0, .125)',
308
+ },
309
+ radioIcon: {
310
+ color: theme?.palette?.tertiary?.contrastText,
311
+ },
312
+ dialog: {
313
+ // minHeight: 500,
314
+ minWidth: 550,
315
+ maxWidth: 800,
316
+ },
317
+ }));
318
+ function checkSumbission(validAsm, sequenceIsEditable, fileType, fastaFile, fastaIndexFile, fastaGziIndexFile, validFastaUrl, validFastaIndexUrl, validFastaGziIndexUrl) {
319
+ if (!validAsm) {
320
+ return false;
321
+ }
322
+ if (sequenceIsEditable && fastaFile) {
323
+ return true;
324
+ }
325
+ if (fileType === FileType.GFF3 && fastaFile) {
326
+ return true;
327
+ }
328
+ if (fastaFile && fastaIndexFile && fastaGziIndexFile) {
329
+ return true;
330
+ }
331
+ if (validFastaUrl && validFastaIndexUrl && validFastaGziIndexUrl) {
332
+ return true;
333
+ }
334
+ return false;
335
+ }
231
336
  function AddAssembly({ changeManager, handleClose, session, }) {
337
+ const { classes } = useStyles$e();
232
338
  const { internetAccounts } = mobxStateTree.getRoot(session);
233
339
  const { notify } = session;
234
340
  const apolloInternetAccounts = internetAccounts.filter((ia) => ia.type === 'ApolloInternetAccount');
@@ -238,50 +344,32 @@ function AddAssembly({ changeManager, handleClose, session, }) {
238
344
  const [assemblyName, setAssemblyName] = React.useState('');
239
345
  const [errorMessage, setErrorMessage] = React.useState('');
240
346
  const [validAsm, setValidAsm] = React.useState(false);
241
- const [file, setFile] = React.useState(null);
242
- const [fileType, setFileType] = React.useState(FileType.GFF3);
347
+ const [fileType, setFileType] = React.useState(FileType.BGZIP_FASTA);
243
348
  const [importFeatures, setImportFeatures] = React.useState(true);
349
+ const [sequenceIsEditable, setSequenceIsEditable] = React.useState(false);
244
350
  const [submitted, setSubmitted] = React.useState(false);
245
- const [selectedInternetAccount, setSelectedInternetAccount] = React.useState(apolloInternetAccounts[0]);
246
- const [fastaFile, setFastaFile] = React.useState('');
247
- const [fastaIndexFile, setFastaIndexFile] = React.useState('');
248
- const [fastaGziIndexFile, setFastaGziIndexFile] = React.useState('');
351
+ const [fastaFile, setFastaFile] = React.useState(null);
352
+ const [fastaIndexFile, setFastaIndexFile] = React.useState(null);
353
+ const [fastaGziIndexFile, setFastaGziIndexFile] = React.useState(null);
354
+ const [fastaUrl, setFastaUrl] = React.useState('');
355
+ const [fastaIndexUrl, setFastaIndexUrl] = React.useState('');
356
+ const [fastaGziIndexUrl, setFastaGziIndexUrl] = React.useState('');
249
357
  const [loading, setLoading] = React.useState(false);
250
- function handleChangeInternetAccount(e) {
251
- setSubmitted(false);
252
- const newlySelectedInternetAccount = apolloInternetAccounts.find((ia) => ia.internetAccountId === e.target.value);
253
- if (!newlySelectedInternetAccount) {
254
- throw new Error(`Could not find internetAccount with ID "${e.target.value}"`);
255
- }
256
- setSelectedInternetAccount(newlySelectedInternetAccount);
257
- }
258
- function handleChangeFile(e) {
259
- if (!e.target.files) {
260
- return;
261
- }
262
- const selectedFile = e.target.files.item(0);
263
- setFile(selectedFile);
264
- if (!selectedFile) {
265
- return;
266
- }
267
- const fileNameLower = selectedFile.name.toLowerCase();
268
- if (fileNameLower.endsWith('.fasta') ||
269
- fileNameLower.endsWith('.fna') ||
270
- fileNameLower.endsWith('.fa')) {
271
- setFileType(FileType.FASTA);
358
+ const [isGzip, setIsGzip] = React.useState(false);
359
+ React.useEffect(() => {
360
+ setFastaIndexUrl(fastaUrl ? `${fastaUrl}.fai` : '');
361
+ }, [fastaUrl]);
362
+ React.useEffect(() => {
363
+ setFastaGziIndexUrl(fastaUrl ? `${fastaUrl}.gzi` : '');
364
+ }, [fastaUrl]);
365
+ React.useEffect(() => {
366
+ if (sequenceIsEditable || fileType === FileType.GFF3) {
367
+ setIsGzip(fastaFile?.name.toLocaleLowerCase().endsWith('.gz') ? true : false);
272
368
  }
273
- else if (fileNameLower.endsWith('.gff3') ||
274
- fileNameLower.endsWith('.gff')) {
275
- setFileType(FileType.GFF3);
369
+ else {
370
+ setIsGzip(true);
276
371
  }
277
- }
278
- function handleChangeFileType(e) {
279
- setFileType(e.target.value);
280
- setImportFeatures(e.target.value === FileType.GFF3);
281
- setFastaFile('');
282
- setFastaIndexFile('');
283
- setFile(null);
284
- }
372
+ }, [fastaFile, sequenceIsEditable, fileType]);
285
373
  function checkAssemblyName(assembly) {
286
374
  const { assemblies } = session;
287
375
  const checkAsm = assemblies.find((asm) => configuration.readConfObject(asm, 'displayName') === assembly);
@@ -294,6 +382,61 @@ function AddAssembly({ changeManager, handleClose, session, }) {
294
382
  setErrorMessage('');
295
383
  }
296
384
  }
385
+ async function uploadFile(file, fileType) {
386
+ const { jobsManager } = session;
387
+ const controller = new AbortController();
388
+ const [{ baseURL, getFetcher }] = apolloInternetAccounts;
389
+ const url = new URL('files', baseURL);
390
+ url.searchParams.set('type', fileType);
391
+ const uri = url.href;
392
+ const formData = new FormData();
393
+ let filename = file.name;
394
+ if (fileType === FileType.FAI || fileType === FileType.GZI) {
395
+ filename = `${filename}.txt`;
396
+ }
397
+ else if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
398
+ filename = `${filename}.gz`;
399
+ }
400
+ else if (!isGzip && file.name.toLocaleLowerCase().endsWith('.gz')) {
401
+ filename = `${filename}.txt`;
402
+ }
403
+ formData.append('file', file, filename);
404
+ formData.append('type', fileType);
405
+ const apolloFetchFile = getFetcher({
406
+ locationType: 'UriLocation',
407
+ uri,
408
+ });
409
+ if (apolloFetchFile) {
410
+ const job = {
411
+ name: `UploadAssemblyFile for ${assemblyName}`,
412
+ statusMessage: 'Pre-validating',
413
+ progressPct: 0,
414
+ cancelCallback: () => {
415
+ controller.abort();
416
+ jobsManager.abortJob(job.name);
417
+ },
418
+ };
419
+ jobsManager.runJob(job);
420
+ jobsManager.update(job.name, `Uploading ${file.name}, this may take awhile`);
421
+ const { signal } = controller;
422
+ const response = await apolloFetchFile(uri, {
423
+ method: 'POST',
424
+ body: formData,
425
+ signal,
426
+ });
427
+ if (!response.ok) {
428
+ const newErrorMessage = await createFetchErrorMessage(response, 'Error when inserting new assembly (while uploading file)');
429
+ jobsManager.abortJob(job.name, newErrorMessage);
430
+ setErrorMessage(newErrorMessage);
431
+ return '';
432
+ }
433
+ const result = await response.json();
434
+ const fileId = result._id;
435
+ jobsManager.done(job);
436
+ return fileId;
437
+ }
438
+ throw new Error('Failed to fetch');
439
+ }
297
440
  async function onSubmit(event) {
298
441
  event.preventDefault();
299
442
  setErrorMessage('');
@@ -302,51 +445,6 @@ function AddAssembly({ changeManager, handleClose, session, }) {
302
445
  notify(`Assembly "${assemblyName}" is being added`, 'info');
303
446
  handleClose();
304
447
  event.preventDefault();
305
- const { jobsManager } = session;
306
- const controller = new AbortController();
307
- const job = {
308
- name: `UploadAssemblyFile for ${assemblyName}`,
309
- statusMessage: 'Pre-validating',
310
- progressPct: 0,
311
- cancelCallback: () => {
312
- controller.abort();
313
- jobsManager.abortJob(job.name);
314
- },
315
- };
316
- jobsManager.runJob(job);
317
- let fileId = '';
318
- const { baseURL, getFetcher, internetAccountId } = selectedInternetAccount;
319
- if (fileType !== FileType.EXTERNAL && file) {
320
- // First upload file
321
- const url = new URL('files', baseURL);
322
- url.searchParams.set('type', fileType);
323
- const uri = url.href;
324
- const formData = new FormData();
325
- formData.append('file', file);
326
- formData.append('fileName', file.name);
327
- formData.append('type', fileType);
328
- const apolloFetchFile = getFetcher({
329
- locationType: 'UriLocation',
330
- uri,
331
- });
332
- if (apolloFetchFile) {
333
- jobsManager.update(job.name, 'Uploading file, this may take awhile');
334
- const { signal } = controller;
335
- const response = await apolloFetchFile(uri, {
336
- method: 'POST',
337
- body: formData,
338
- signal,
339
- });
340
- if (!response.ok) {
341
- const newErrorMessage = await createFetchErrorMessage(response, 'Error when inserting new assembly (while uploading file)');
342
- jobsManager.abortJob(job.name, newErrorMessage);
343
- setErrorMessage(newErrorMessage);
344
- return;
345
- }
346
- const result = await response.json();
347
- fileId = result._id;
348
- }
349
- }
350
448
  let change;
351
449
  if (fileType === FileType.EXTERNAL) {
352
450
  change = new shared.AddAssemblyFromExternalChange({
@@ -354,30 +452,67 @@ function AddAssembly({ changeManager, handleClose, session, }) {
354
452
  assembly: new ObjectID__default["default"]().toHexString(),
355
453
  assemblyName,
356
454
  externalLocation: {
357
- fa: fastaFile,
358
- fai: fastaIndexFile,
359
- ...(fastaGziIndexFile ? { gzi: fastaGziIndexFile } : {}),
455
+ fa: fastaUrl,
456
+ fai: fastaIndexUrl,
457
+ gzi: fastaGziIndexUrl,
360
458
  },
361
459
  });
362
460
  }
363
461
  else {
364
- const fileUploadChangeBase = {
365
- assembly: new ObjectID__default["default"]().toHexString(),
366
- assemblyName,
367
- fileIds: { fa: fileId },
368
- };
369
- change =
370
- fileType === FileType.GFF3 && importFeatures
371
- ? new shared.AddAssemblyAndFeaturesFromFileChange({
372
- typeName: 'AddAssemblyAndFeaturesFromFileChange',
373
- ...fileUploadChangeBase,
374
- })
375
- : new shared.AddAssemblyFromFileChange({
376
- typeName: 'AddAssemblyFromFileChange',
377
- ...fileUploadChangeBase,
378
- });
462
+ if (!fastaFile) {
463
+ throw new Error('Missing fasta file');
464
+ }
465
+ if (fileType === FileType.GFF3 && importFeatures) {
466
+ const faId = await uploadFile(fastaFile, FileType.GFF3);
467
+ change = new shared.AddAssemblyAndFeaturesFromFileChange({
468
+ typeName: 'AddAssemblyAndFeaturesFromFileChange',
469
+ assembly: new ObjectID__default["default"]().toHexString(),
470
+ assemblyName,
471
+ fileIds: { fa: faId },
472
+ });
473
+ }
474
+ else if (fileType === FileType.GFF3) {
475
+ const faId = await uploadFile(fastaFile, FileType.GFF3);
476
+ change = new shared.AddAssemblyFromFileChange({
477
+ typeName: 'AddAssemblyFromFileChange',
478
+ assembly: new ObjectID__default["default"]().toHexString(),
479
+ assemblyName,
480
+ fileIds: {
481
+ fa: faId,
482
+ },
483
+ });
484
+ }
485
+ else if (sequenceIsEditable) {
486
+ const faId = await uploadFile(fastaFile, FileType.FASTA);
487
+ change = new shared.AddAssemblyFromFileChange({
488
+ typeName: 'AddAssemblyFromFileChange',
489
+ assembly: new ObjectID__default["default"]().toHexString(),
490
+ assemblyName,
491
+ fileIds: {
492
+ fa: faId,
493
+ },
494
+ });
495
+ }
496
+ else {
497
+ if (!fastaIndexFile || !fastaGziIndexFile) {
498
+ throw new Error('Missing fasta index files');
499
+ }
500
+ const faId = await uploadFile(fastaFile, FileType.BGZIP_FASTA);
501
+ const faiId = await uploadFile(fastaIndexFile, FileType.FAI);
502
+ const gziId = await uploadFile(fastaGziIndexFile, FileType.GZI);
503
+ change = new shared.AddAssemblyFromFileChange({
504
+ typeName: 'AddAssemblyFromFileChange',
505
+ assembly: new ObjectID__default["default"]().toHexString(),
506
+ assemblyName,
507
+ fileIds: {
508
+ fa: faId,
509
+ fai: faiId,
510
+ gzi: gziId,
511
+ },
512
+ });
513
+ }
379
514
  }
380
- jobsManager.done(job);
515
+ const [{ internetAccountId }] = apolloInternetAccounts;
381
516
  await changeManager.submit(change, {
382
517
  internetAccountId,
383
518
  updateJobsManager: true,
@@ -385,89 +520,175 @@ function AddAssembly({ changeManager, handleClose, session, }) {
385
520
  setSubmitted(false);
386
521
  setLoading(false);
387
522
  }
388
- let validFastaFile = false;
523
+ let validFastaUrl = false;
389
524
  try {
390
- const url = new URL(fastaFile);
525
+ const url = new URL(fastaUrl);
391
526
  if (url.protocol === 'http:' || url.protocol === 'https:') {
392
- validFastaFile = true;
527
+ validFastaUrl = true;
393
528
  }
394
529
  }
395
530
  catch {
396
531
  // pass
397
532
  }
398
- let validFastaIndexFile = false;
533
+ let validFastaIndexUrl = false;
399
534
  try {
400
- const url = new URL(fastaIndexFile);
535
+ const url = new URL(fastaIndexUrl);
401
536
  if (url.protocol === 'http:' || url.protocol === 'https:') {
402
- validFastaIndexFile = true;
537
+ validFastaIndexUrl = true;
403
538
  }
404
539
  }
405
540
  catch {
406
541
  // pass
407
542
  }
408
- let validFastaGziIndexFile = false;
543
+ let validFastaGziIndexUrl = false;
409
544
  try {
410
- const url = new URL(fastaGziIndexFile);
545
+ const url = new URL(fastaGziIndexUrl);
411
546
  if (url.protocol === 'http:' || url.protocol === 'https:') {
412
- validFastaGziIndexFile = true;
547
+ validFastaGziIndexUrl = true;
413
548
  }
414
549
  }
415
550
  catch {
416
551
  // pass
417
552
  }
418
- return (React__default["default"].createElement(Dialog, { open: true, maxWidth: false, "data-testid": "add-assembly-dialog", title: "Add new assembly", handleClose: handleClose },
419
- loading ? React__default["default"].createElement(LinearProgress__default["default"], null) : null,
420
- React__default["default"].createElement("form", { onSubmit: onSubmit },
421
- React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
422
- apolloInternetAccounts.length > 1 ? (React__default["default"].createElement(React__default["default"].Fragment, null,
423
- React__default["default"].createElement(material.DialogContentText, null, "Select account"),
424
- React__default["default"].createElement(material.Select, { value: selectedInternetAccount.internetAccountId, onChange: handleChangeInternetAccount, disabled: submitted && !errorMessage }, internetAccounts.map((option) => (React__default["default"].createElement(material.MenuItem, { key: option.id, value: option.internetAccountId }, option.name)))))) : null,
553
+ const [expanded, setExpanded] = React__default["default"].useState('panelFastaInput');
554
+ const handleAccordionChange = (panel) => (event, newExpanded) => {
555
+ if (newExpanded) {
556
+ setExpanded(panel);
557
+ }
558
+ if (panel === 'panelGffInput') {
559
+ setIsGzip(false);
560
+ }
561
+ else {
562
+ setIsGzip(true);
563
+ }
564
+ };
565
+ return (React__default["default"].createElement(Dialog, { open: true, handleClose: handleClose, "data-testid": "add-assembly-dialog", title: "Add new assembly", maxWidth: false },
566
+ React__default["default"].createElement("form", { onSubmit: onSubmit, "data-testid": "submit-form" },
567
+ React__default["default"].createElement(material.DialogContent, { className: classes.dialog },
568
+ loading ? React__default["default"].createElement(material.LinearProgress, null) : null,
425
569
  React__default["default"].createElement(material.TextField, { margin: "dense", id: "name", label: "Assembly name", type: "TextField", fullWidth: true, variant: "outlined", onChange: (e) => {
426
570
  setSubmitted(false);
427
571
  setAssemblyName(e.target.value);
428
572
  checkAssemblyName(e.target.value);
429
573
  }, disabled: submitted && !errorMessage }),
430
- React__default["default"].createElement(material.FormControl, { style: { marginTop: 20 } },
431
- React__default["default"].createElement(material.FormLabel, null, "Select GFF3, FASTA or EXTERNAL option"),
432
- React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: FileType.GFF3, name: "radio-buttons-group", onChange: handleChangeFileType, value: fileType },
433
- React__default["default"].createElement(material.FormControlLabel, { value: FileType.GFF3, control: React__default["default"].createElement(material.Radio, null), label: "GFF3", disabled: submitted && !errorMessage }),
434
- React__default["default"].createElement(material.FormControlLabel, { value: FileType.FASTA, control: React__default["default"].createElement(material.Radio, null), label: "FASTA", disabled: submitted && !errorMessage }),
435
- React__default["default"].createElement(material.FormControlLabel, { value: FileType.EXTERNAL, control: React__default["default"].createElement(material.Radio, null), label: "External", disabled: submitted && !errorMessage }))),
436
- fileType === FileType.EXTERNAL ? (React__default["default"].createElement(material.Box, { style: { marginTop: 20 } },
437
- React__default["default"].createElement(material.Typography, { variant: "caption" }, "Enter FASTA and FASTA index(es) URL"),
438
- React__default["default"].createElement(material.TextField, { margin: "dense", helperText: "Can be bgz-compressed", id: "fasta", label: "FASTA", type: "TextField", fullWidth: true, variant: "outlined", error: !validFastaFile, onChange: (e) => {
439
- setFastaFile(e.target.value);
440
- }, disabled: submitted && !errorMessage, InputProps: {
441
- startAdornment: (React__default["default"].createElement(InputAdornment__default["default"], { position: "start" },
442
- React__default["default"].createElement(LinkIcon__default["default"], null))),
443
- } }),
444
- React__default["default"].createElement(material.TextField, { margin: "dense", id: "fasta-index", label: "FASTA Index", helperText: ".fai or .gz.fai", type: "TextField", fullWidth: true, variant: "outlined", error: !validFastaIndexFile, onChange: (e) => {
445
- setFastaIndexFile(e.target.value);
446
- }, disabled: submitted && !errorMessage, InputProps: {
447
- startAdornment: (React__default["default"].createElement(InputAdornment__default["default"], { position: "start" },
448
- React__default["default"].createElement(LinkIcon__default["default"], null))),
449
- } }),
450
- React__default["default"].createElement(material.TextField, { margin: "dense", id: "fasta-gzi-index", label: "FASTA GZI Index", helperText: "Only for bgz-compressed FASTA, .gz.gzi", type: "TextField", fullWidth: true, variant: "outlined", error: Boolean(fastaGziIndexFile) && !validFastaGziIndexFile, onChange: (e) => {
451
- setFastaGziIndexFile(e.target.value);
452
- }, disabled: submitted && !errorMessage, InputProps: {
453
- startAdornment: (React__default["default"].createElement(InputAdornment__default["default"], { position: "start" },
454
- React__default["default"].createElement(LinkIcon__default["default"], null))),
455
- } }))) : (React__default["default"].createElement(material.Box, { style: { marginTop: 20 } },
456
- React__default["default"].createElement("input", { type: "file", onChange: handleChangeFile, disabled: submitted && !errorMessage }),
457
- React__default["default"].createElement(material.FormGroup, null,
458
- React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { checked: fileType === FileType.GFF3 && importFeatures, onChange: () => {
459
- setImportFeatures(!importFeatures);
460
- }, disabled: fileType !== FileType.GFF3 ||
461
- (submitted && !errorMessage) }), label: "Also load features from GFF3 file" }))))),
574
+ React__default["default"].createElement(material.Accordion, { disableGutters: true, elevation: 0, square: true, className: classes.accordion, expanded: expanded === 'panelFastaInput', onChange: handleAccordionChange('panelFastaInput') },
575
+ React__default["default"].createElement(material.AccordionSummary, { className: classes.accordionSummary, expandIcon: expanded === 'panelFastaInput' ? (React__default["default"].createElement(RadioButtonCheckedIcon__default["default"], { className: classes.radioIcon, sx: { fontSize: '1.2rem', ml: 5 } })) : (React__default["default"].createElement(RadioButtonUncheckedIcon__default["default"], { className: classes.radioIcon, sx: { fontSize: '1.2rem', mr: 5 } })), "aria-controls": "panelFastaInputd-content", id: "panelFastaInputd-header" },
576
+ React__default["default"].createElement(material.Typography, { component: "span" }, "FASTA input")),
577
+ React__default["default"].createElement(material.AccordionDetails, { className: classes.accordionDetails },
578
+ React__default["default"].createElement(material.FormGroup, null,
579
+ React__default["default"].createElement(material.FormControlLabel, { "data-testid": "files-on-url-checkbox", control: React__default["default"].createElement(material.Checkbox, { onChange: () => {
580
+ setFileType(fileType === FileType.EXTERNAL
581
+ ? FileType.BGZIP_FASTA
582
+ : FileType.EXTERNAL);
583
+ if (fileType === FileType.EXTERNAL) {
584
+ setSequenceIsEditable(false);
585
+ }
586
+ }, checked: fileType === FileType.EXTERNAL, disabled: sequenceIsEditable && fileType !== FileType.GFF3 }), label: React__default["default"].createElement(material.Box, { display: "flex", alignItems: "center" },
587
+ "Use external URLs",
588
+ React__default["default"].createElement(material.Tooltip, { title: "Use external URLs to provide FASTA and index files. Does not copy the files to the Apollo collaboration server, so ensure the URLs are stable.", placement: "top-start" },
589
+ React__default["default"].createElement(material.IconButton, { size: "small" },
590
+ React__default["default"].createElement(InfoIcon__default["default"], { sx: { fontSize: 18 } })))) }),
591
+ React__default["default"].createElement(material.FormControlLabel, { "data-testid": "sequence-is-editable-checkbox", control: React__default["default"].createElement(material.Checkbox, { onChange: () => {
592
+ setSequenceIsEditable(!sequenceIsEditable);
593
+ } }), checked: sequenceIsEditable, disabled: fileType === FileType.EXTERNAL, label: React__default["default"].createElement(material.Box, { display: "flex", alignItems: "center" },
594
+ "Store sequence in database",
595
+ React__default["default"].createElement(material.Tooltip, { title: "Enables users to edit the genomic sequence, but comes with performance impacts. Use with care.", placement: "top-start" },
596
+ React__default["default"].createElement(material.IconButton, { size: "small" },
597
+ React__default["default"].createElement(InfoIcon__default["default"], { sx: { fontSize: 18 } })))) }),
598
+ React__default["default"].createElement(material.FormControlLabel, { "data-testid": "fasta-is-gzip-checkbox", control: React__default["default"].createElement(material.Checkbox, { checked: isGzip, onChange: () => {
599
+ if (sequenceIsEditable) {
600
+ setIsGzip(!isGzip);
601
+ }
602
+ else {
603
+ setIsGzip(true);
604
+ }
605
+ }, disabled: !sequenceIsEditable }), label: "FASTA is gzip compressed" }),
606
+ fileType === FileType.BGZIP_FASTA ||
607
+ fileType === FileType.GFF3 ? (React__default["default"].createElement(material.Table, { size: "small", sx: { mt: 2 } },
608
+ React__default["default"].createElement(material.TableBody, null,
609
+ React__default["default"].createElement(material.TableRow, null),
610
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
611
+ React__default["default"].createElement(material.Box, { display: "flex", alignItems: "center" },
612
+ React__default["default"].createElement("span", null, "FASTA"),
613
+ React__default["default"].createElement(material.Tooltip, { title: 'Unless "Store sequence in database" enabled, FASTA input must be compressed with bgzip and indexed with samtools faidx (or equivalent). Compression is optional for sequences stored in the database.' },
614
+ React__default["default"].createElement(material.IconButton, { size: "small" },
615
+ React__default["default"].createElement(InfoIcon__default["default"], { sx: { fontSize: 18 } }))))),
616
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
617
+ React__default["default"].createElement("input", { "data-testid": "fasta-input-file", type: "file", onChange: (e) => {
618
+ setFastaFile(e.target.files?.item(0) ?? null);
619
+ }, disabled: submitted && !errorMessage })),
620
+ React__default["default"].createElement(material.TableRow, null),
621
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } }, "FASTA index (.fai)"),
622
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
623
+ React__default["default"].createElement("input", { "data-testid": "fai-input-file", type: "file", onChange: (e) => {
624
+ setFastaIndexFile(e.target.files?.item(0) ?? null);
625
+ }, disabled: (submitted && !errorMessage) || sequenceIsEditable })),
626
+ React__default["default"].createElement(material.TableRow, null),
627
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } }, "FASTA binary index (.gzi)"),
628
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
629
+ React__default["default"].createElement("input", { "data-testid": "gzi-input-file", type: "file", onChange: (e) => {
630
+ setFastaGziIndexFile(e.target.files?.item(0) ?? null);
631
+ }, disabled: (submitted && !errorMessage) || sequenceIsEditable }))))) : (React__default["default"].createElement(material.Table, { size: "small", sx: { mt: 2 } },
632
+ React__default["default"].createElement(material.TableBody, null,
633
+ React__default["default"].createElement(material.TableRow, null),
634
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
635
+ React__default["default"].createElement(material.Box, { display: "flex", alignItems: "center" },
636
+ React__default["default"].createElement("span", null, "FASTA"),
637
+ React__default["default"].createElement(material.Tooltip, { title: "Remote FASTA input must be compressed with bgzip and indexed with samtools faidx (or equivalent)" },
638
+ React__default["default"].createElement(material.IconButton, { size: "small" },
639
+ React__default["default"].createElement(InfoIcon__default["default"], { sx: { fontSize: 18 } }))))),
640
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
641
+ React__default["default"].createElement(material.TextField, { "data-testid": "fasta-input-url", variant: "outlined", value: fastaUrl, error: !validFastaUrl, onChange: (e) => {
642
+ setFastaUrl(e.target.value);
643
+ }, disabled: submitted && !errorMessage, slotProps: {
644
+ input: {
645
+ startAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "start" },
646
+ React__default["default"].createElement(LinkIcon__default["default"], null))),
647
+ },
648
+ } })),
649
+ React__default["default"].createElement(material.TableRow, null),
650
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } }, "FASTA index (.fai)"),
651
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
652
+ React__default["default"].createElement(material.TextField, { "data-testid": "fai-input-url", variant: "outlined", value: fastaIndexUrl, error: !validFastaIndexUrl, onChange: (e) => {
653
+ setFastaIndexUrl(e.target.value);
654
+ }, disabled: submitted && !errorMessage, slotProps: {
655
+ input: {
656
+ startAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "start" },
657
+ React__default["default"].createElement(LinkIcon__default["default"], null))),
658
+ },
659
+ } })),
660
+ React__default["default"].createElement(material.TableRow, null),
661
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } }, "FASTA binary index (.gzi)"),
662
+ React__default["default"].createElement(material.TableCell, { style: { borderBottomWidth: 0 } },
663
+ React__default["default"].createElement(material.TextField, { "data-testid": "gzi-input-url", variant: "outlined", value: fastaGziIndexUrl, error: !validFastaGziIndexUrl, onChange: (e) => {
664
+ setFastaGziIndexUrl(e.target.value);
665
+ }, disabled: submitted && !errorMessage, slotProps: {
666
+ input: {
667
+ startAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "start" },
668
+ React__default["default"].createElement(LinkIcon__default["default"], null))),
669
+ },
670
+ } })))))))),
671
+ React__default["default"].createElement(material.Accordion, { disableGutters: true, elevation: 0, square: true, className: classes.accordion, expanded: expanded === 'panelGffInput', onChange: handleAccordionChange('panelGffInput') },
672
+ React__default["default"].createElement(material.AccordionSummary, { className: classes.accordionSummary, expandIcon: expanded === 'panelGffInput' ? (React__default["default"].createElement(RadioButtonCheckedIcon__default["default"], { className: classes.radioIcon, sx: { fontSize: '1.2rem', ml: 5 } })) : (React__default["default"].createElement(RadioButtonUncheckedIcon__default["default"], { className: classes.radioIcon, sx: { fontSize: '1.2rem', mr: 5 } })), "aria-controls": "panelGffInputd-content" },
673
+ React__default["default"].createElement(material.Typography, { component: "span" },
674
+ "GFF3 input",
675
+ React__default["default"].createElement(material.Tooltip, { title: "GFF3 must includes FASTA sequences. File can be gzip compressed." },
676
+ React__default["default"].createElement(InfoIcon__default["default"], { className: classes.radioIcon, sx: { fontSize: 18 } })))),
677
+ React__default["default"].createElement(material.AccordionDetails, { className: classes.accordionDetails },
678
+ React__default["default"].createElement(material.Box, { style: { marginTop: 20 } },
679
+ React__default["default"].createElement("input", { "data-testid": "gff3-input-file", type: "file", disabled: submitted && !errorMessage, onChange: (e) => {
680
+ setFastaFile(e.target.files?.item(0) ?? null);
681
+ setFileType(FileType.GFF3);
682
+ } }),
683
+ React__default["default"].createElement(material.FormGroup, { style: { display: 'grid' } },
684
+ React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { checked: importFeatures, onChange: () => {
685
+ setImportFeatures(!importFeatures);
686
+ }, disabled: submitted && !errorMessage }), label: "Load features from GFF3 file" }),
687
+ React__default["default"].createElement(material.FormControlLabel, { "data-testid": "gff3-is-gzip-checkbox", control: React__default["default"].createElement(material.Checkbox, { checked: isGzip, onChange: () => {
688
+ setIsGzip(!isGzip);
689
+ }, disabled: submitted && !errorMessage }), label: "GFF3 is gzip compressed" })))))),
462
690
  React__default["default"].createElement(material.DialogActions, null,
463
- React__default["default"].createElement(material.Button, { disabled: !validAsm ||
464
- !((assemblyName && file) ??
465
- (assemblyName &&
466
- fastaFile &&
467
- fastaIndexFile &&
468
- validFastaFile &&
469
- validFastaIndexFile)) ||
470
- submitted, variant: "contained", type: "submit" }, submitted ? 'Submitting...' : 'Submit'),
691
+ React__default["default"].createElement(material.Button, { disabled: !checkSumbission(validAsm, sequenceIsEditable, fileType, fastaFile, fastaIndexFile, fastaGziIndexFile, validFastaUrl, validFastaIndexUrl, validFastaGziIndexUrl) || submitted, variant: "contained", type: "submit", "data-testid": "submit-button" }, submitted ? 'Submitting...' : 'Submit'),
471
692
  React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
472
693
  errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
473
694
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
@@ -602,7 +823,16 @@ const genericEnglishStopwords = new Set([
602
823
  'don',
603
824
  'should',
604
825
  'now',
605
- ...'1234567890',
826
+ '0',
827
+ '1',
828
+ '2',
829
+ '3',
830
+ '4',
831
+ '5',
832
+ '6',
833
+ '7',
834
+ '8',
835
+ '9',
606
836
  ]);
607
837
  /**
608
838
  * The set of stopwords we use for fulltext indexing. Currently
@@ -1339,7 +1569,9 @@ const OntologyRecordType = mobxStateTree.types
1339
1569
  return;
1340
1570
  }
1341
1571
  void self.loadEquivalentTypes('gene');
1572
+ void self.loadEquivalentTypes('pseudogene');
1342
1573
  void self.loadEquivalentTypes('transcript');
1574
+ void self.loadEquivalentTypes('pseudogenic_transcript');
1343
1575
  void self.loadEquivalentTypes('CDS');
1344
1576
  void self.loadEquivalentTypes('mRNA');
1345
1577
  reaction.dispose();
@@ -2637,451 +2869,39 @@ function ManageUsers({ changeManager, handleClose, session, }) {
2637
2869
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
2638
2870
  }
2639
2871
 
2640
- /* eslint-disable @typescript-eslint/unbound-method */
2641
- // interface TermAutocompleteResult extends TermValue {
2642
- // label: string[]
2643
- // match: string
2644
- // category: string[]
2645
- // taxon: string
2646
- // taxon_label: string
2647
- // highlight: string
2648
- // has_highlight: boolean
2649
- // }
2650
- // interface TermAutocompleteResponse {
2651
- // docs: TermAutocompleteResult[]
2652
- // }
2653
- // const hiliteRegex = /(?<=<em class="hilite">)(.*?)(?=<\/em>)/g
2654
- function TermTagWithTooltip({ getTagProps, index, ontology, termId, }) {
2655
- const manager = mobxStateTree.getParent(ontology, 2);
2656
- const [description, setDescription] = React__namespace.useState('');
2657
- const [errorMessage, setErrorMessage] = React__namespace.useState('');
2658
- React__namespace.useEffect(() => {
2659
- const controller = new AbortController();
2660
- const { signal } = controller;
2661
- async function fetchDescription() {
2662
- const termUrl = manager.expandPrefixes(termId);
2663
- const db = await ontology.dataStore?.db;
2664
- if (!db || signal.aborted) {
2665
- return;
2872
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2873
+ function OpenLocalFile({ handleClose, session }) {
2874
+ const { apolloDataStore } = session;
2875
+ const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
2876
+ const [file, setFile] = React.useState(null);
2877
+ const [assemblyName, setAssemblyName] = React.useState('');
2878
+ const [errorMessage, setErrorMessage] = React.useState('');
2879
+ const [submitted, setSubmitted] = React.useState(false);
2880
+ const theme = material.useTheme();
2881
+ function handleChangeFile(e) {
2882
+ const selectedFile = e.target.files?.item(0);
2883
+ if (!selectedFile) {
2884
+ return;
2885
+ }
2886
+ setErrorMessage('');
2887
+ setFile(selectedFile);
2888
+ if (!assemblyName) {
2889
+ const fileName = selectedFile.name;
2890
+ const lastDotIndex = fileName.lastIndexOf('.');
2891
+ if (lastDotIndex === -1) {
2892
+ setAssemblyName(fileName);
2666
2893
  }
2667
- const term = await db
2668
- .transaction('nodes')
2669
- .objectStore('nodes')
2670
- .get(termUrl);
2671
- if (term && term.lbl && !signal.aborted) {
2672
- setDescription(term.lbl || 'no label');
2894
+ else {
2895
+ setAssemblyName(fileName.slice(0, lastDotIndex));
2673
2896
  }
2674
2897
  }
2675
- fetchDescription().catch((error) => {
2676
- if (!signal.aborted) {
2677
- setErrorMessage(String(error));
2678
- }
2679
- });
2680
- return () => {
2681
- controller.abort();
2682
- };
2683
- }, [termId, ontology, manager]);
2684
- return (React__namespace.createElement(material.Tooltip, { title: description },
2685
- React__namespace.createElement("div", null,
2686
- React__namespace.createElement(material.Chip, { label: errorMessage || manager.applyPrefixes(termId), color: errorMessage ? 'error' : 'default', size: "small", ...getTagProps({ index }) }))));
2687
- }
2688
- function OntologyTermMultiSelect({ includeDeprecated, onChange, ontologyName, ontologyVersion, session, value: initialValue, }) {
2689
- const { ontologyManager } = session.apolloDataStore;
2690
- const ontology = ontologyManager.findOntology(ontologyName, ontologyVersion);
2691
- const [value, setValue] = React__namespace.useState(initialValue.map((id) => ({ term: { id, type: 'CLASS' } })));
2692
- const [inputValue, setInputValue] = React__namespace.useState('');
2693
- const [options, setOptions] = React__namespace.useState([]);
2694
- const [loading, setLoading] = React__namespace.useState(false);
2695
- const [errorMessage, setErrorMessage] = React__namespace.useState('');
2696
- const getOntologyTerms = React__namespace.useMemo(() => utils.debounce(async (request, callback) => {
2697
- if (!ontology) {
2698
- return;
2699
- }
2700
- const { dataStore } = ontology;
2701
- if (!dataStore) {
2702
- return;
2703
- }
2704
- const { input, signal } = request;
2705
- try {
2706
- const matches = await dataStore.getTermsByFulltext(input, undefined, signal);
2707
- // aggregate the matches by term
2708
- const byTerm = new Map();
2709
- const options = [];
2710
- for (const match of matches) {
2711
- if (!isOntologyClass(match.term) ||
2712
- (!includeDeprecated && isDeprecated(match.term))) {
2713
- continue;
2714
- }
2715
- let slot = byTerm.get(match.term.id);
2716
- if (!slot) {
2717
- slot = { term: match.term, matches: [] };
2718
- byTerm.set(match.term.id, slot);
2719
- options.push(slot);
2720
- }
2721
- slot.matches.push(match);
2722
- }
2723
- callback(options);
2724
- }
2725
- catch (error) {
2726
- if (!aborting.isAbortException(error)) {
2727
- setErrorMessage(String(error));
2728
- }
2729
- }
2730
- }, 400), [includeDeprecated, ontology]);
2731
- React__namespace.useEffect(() => {
2732
- const aborter = new AbortController();
2733
- const { signal } = aborter;
2734
- if (inputValue === '') {
2735
- setOptions([]);
2736
- return;
2737
- }
2738
- setLoading(true);
2739
- void getOntologyTerms({ input: inputValue, signal }, (results) => {
2740
- let newOptions = [];
2741
- if (value.length > 0) {
2742
- newOptions = value;
2743
- }
2744
- if (results) {
2745
- newOptions = [...newOptions, ...results];
2746
- }
2747
- setOptions(newOptions);
2748
- setLoading(false);
2749
- });
2750
- return () => {
2751
- aborter.abort();
2752
- };
2753
- }, [getOntologyTerms, ontology, includeDeprecated, inputValue, value]);
2754
- if (!ontology) {
2755
- return null;
2756
- }
2757
- const extraTextFieldParams = {};
2758
- if (errorMessage) {
2759
- extraTextFieldParams.error = true;
2760
- extraTextFieldParams.helperText = errorMessage;
2761
- }
2762
- return (React__namespace.createElement(material.Autocomplete, { getOptionLabel: (option) => ontologyManager.applyPrefixes(option.term.id), filterOptions: (terms) => terms.filter((t) => isOntologyClass(t.term)), options: options, autoComplete: true, includeInputInList: true, filterSelectedOptions: true, value: value, loading: loading, isOptionEqualToValue: (option, v) => ontologyManager.applyPrefixes(option.term.id) ===
2763
- ontologyManager.applyPrefixes(v.term.id), noOptionsText: inputValue ? 'No matches' : 'Start typing to search', onChange: (_, newValue) => {
2764
- setOptions(newValue ? [...newValue, ...options] : options);
2765
- onChange(newValue.map((v) => ontologyManager.applyPrefixes(v.term.id)));
2766
- setValue(newValue);
2767
- }, onInputChange: (event, newInputValue) => {
2768
- if (newInputValue) {
2769
- setLoading(true);
2770
- }
2771
- setOptions([]);
2772
- setInputValue(newInputValue);
2773
- }, multiple: true, renderInput: (params) => (React__namespace.createElement(material.TextField, { ...params, ...extraTextFieldParams, variant: "outlined", fullWidth: true })), renderOption: (props, option) => (React__namespace.createElement(Option, { ...props, ontologyManager: ontologyManager, option: option, inputValue: inputValue })), renderTags: (v, getTagProps) => v.map((option, index) => (React__namespace.createElement(TermTagWithTooltip, { termId: option.term.id, index: index, ontology: ontology, getTagProps: getTagProps, key: option.term.id }))) }));
2774
- }
2775
- function HighlightedText(props) {
2776
- const { search, str } = props;
2777
- const highlights = highlightMatch__default["default"](str, search, {
2778
- insideWords: true,
2779
- findAllOccurrences: true,
2780
- });
2781
- const parts = highlightParse__default["default"](str, highlights);
2782
- return (React__namespace.createElement(React__namespace.Fragment, null, parts.map((part, index) => (React__namespace.createElement(material.Typography, { key: index, component: "span", sx: { fontWeight: part.highlight ? 'bold' : 'regular' }, variant: "body2", color: "text.secondary" }, part.text)))));
2783
- }
2784
- function Option(props) {
2785
- const { inputValue, ontologyManager, option, ...other } = props;
2786
- const matches = option.matches ?? [];
2787
- const fields = matches
2788
- .filter((match) => match.field.jsonPath !== '$.lbl')
2789
- .map((match) => {
2790
- return (React__namespace.createElement(React__namespace.Fragment, { key: `option-${match.term.id}-${match.str}` },
2791
- React__namespace.createElement(material.Typography, { component: "dt", variant: "body2", color: "text.secondary" }, match.field.displayName),
2792
- React__namespace.createElement("dd", null,
2793
- React__namespace.createElement(HighlightedText, { str: match.str, search: inputValue }))));
2794
- });
2795
- // const lblScore = matches
2796
- // .filter((match) => match.field.jsonPath === '$.lbl')
2797
- // .map((m) => m.score)
2798
- // .join(', ')
2799
- return (React__namespace.createElement("li", { ...other },
2800
- React__namespace.createElement(material.Grid2, { container: true },
2801
- React__namespace.createElement(material.Grid2, null,
2802
- React__namespace.createElement(material.Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
2803
- ' ',
2804
- React__namespace.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
2805
- ' ',
2806
- React__namespace.createElement("dl", null, fields)))));
2807
- }
2808
-
2809
- const reservedKeys$1 = new Map([
2810
- [
2811
- 'Gene Ontology',
2812
- (props) => {
2813
- return React__default["default"].createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Gene Ontology" });
2814
- },
2815
- ],
2816
- [
2817
- 'Sequence Ontology',
2818
- (props) => {
2819
- return (React__default["default"].createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Sequence Ontology" }));
2820
- },
2821
- ],
2822
- ]);
2823
- const useStyles$e = mui.makeStyles()((theme) => ({
2824
- attributeInput: {
2825
- maxWidth: 600,
2826
- },
2827
- newAttributePaper: {
2828
- padding: theme.spacing(2),
2829
- },
2830
- attributeName: {
2831
- background: theme.palette.secondary.main,
2832
- color: theme.palette.secondary.contrastText,
2833
- padding: theme.spacing(1),
2834
- },
2835
- }));
2836
- const reservedTerms$1 = [
2837
- 'ID',
2838
- 'Name',
2839
- 'Alias',
2840
- 'Target',
2841
- 'Gap',
2842
- 'Derives_from',
2843
- 'Note',
2844
- 'Dbxref',
2845
- 'Ontology',
2846
- 'Is_Circular',
2847
- ];
2848
- function CustomAttributeValueEditor$1(props) {
2849
- const { onChange, value } = props;
2850
- return (React__default["default"].createElement(material.TextField, { type: "text", value: value, onChange: (event) => {
2851
- onChange(event.target.value.split(','));
2852
- }, variant: "outlined", fullWidth: true, helperText: "Separate multiple values for the attribute with commas" }));
2853
- }
2854
- function ModifyFeatureAttribute({ changeManager, handleClose, session, sourceAssemblyId, sourceFeature, }) {
2855
- const { notify } = session;
2856
- const { internetAccounts } = mobxStateTree.getRoot(session);
2857
- const internetAccount = React.useMemo(() => {
2858
- return internetAccounts.find((ia) => ia.type === 'ApolloInternetAccount');
2859
- }, [internetAccounts]);
2860
- const role = internetAccount ? internetAccount.role : 'admin';
2861
- const editable = ['admin', 'user'].includes(role ?? '');
2862
- const [errorMessage, setErrorMessage] = React.useState('');
2863
- const [attributes, setAttributes] = React.useState(Object.fromEntries([...sourceFeature.attributes.entries()].map(([key, value]) => {
2864
- if (key.startsWith('gff_')) {
2865
- const newKey = key.slice(4);
2866
- const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1);
2867
- return [capitalizedKey, mobxStateTree.getSnapshot(value)];
2868
- }
2869
- if (key === '_id') {
2870
- return ['ID', mobxStateTree.getSnapshot(value)];
2871
- }
2872
- return [key, mobxStateTree.getSnapshot(value)];
2873
- })));
2874
- const [showAddNewForm, setShowAddNewForm] = React.useState(false);
2875
- const [newAttributeKey, setNewAttributeKey] = React.useState('');
2876
- const { classes } = useStyles$e();
2877
- async function onSubmit(event) {
2878
- event.preventDefault();
2879
- setErrorMessage('');
2880
- const attrs = {};
2881
- if (attributes) {
2882
- for (const [key, val] of Object.entries(attributes)) {
2883
- if (!val) {
2884
- continue;
2885
- }
2886
- const newKey = key.toLowerCase();
2887
- if (newKey === 'parent') {
2888
- continue;
2889
- }
2890
- if ([...reservedKeys$1.keys()].includes(key)) {
2891
- attrs[key] = val;
2892
- continue;
2893
- }
2894
- switch (key) {
2895
- case 'ID': {
2896
- attrs._id = val;
2897
- break;
2898
- }
2899
- case 'Name': {
2900
- attrs.gff_name = val;
2901
- break;
2902
- }
2903
- case 'Alias': {
2904
- attrs.gff_alias = val;
2905
- break;
2906
- }
2907
- case 'Target': {
2908
- attrs.gff_target = val;
2909
- break;
2910
- }
2911
- case 'Gap': {
2912
- attrs.gff_gap = val;
2913
- break;
2914
- }
2915
- case 'Derives_from': {
2916
- attrs.gff_derives_from = val;
2917
- break;
2918
- }
2919
- case 'Note': {
2920
- attrs.gff_note = val;
2921
- break;
2922
- }
2923
- case 'Dbxref': {
2924
- attrs.gff_dbxref = val;
2925
- break;
2926
- }
2927
- case 'Ontology_term': {
2928
- attrs.gff_ontology_term = val;
2929
- break;
2930
- }
2931
- case 'Is_circular': {
2932
- attrs.gff_is_circular = val;
2933
- break;
2934
- }
2935
- default: {
2936
- attrs[key.toLowerCase()] = val;
2937
- }
2938
- }
2939
- }
2940
- }
2941
- const change = new shared.FeatureAttributeChange({
2942
- changedIds: [sourceFeature._id],
2943
- typeName: 'FeatureAttributeChange',
2944
- assembly: sourceAssemblyId,
2945
- featureId: sourceFeature._id,
2946
- attributes: attrs,
2947
- });
2948
- await changeManager.submit(change);
2949
- notify('Feature attributes modified successfully', 'success');
2950
- handleClose();
2951
- event.preventDefault();
2952
- }
2953
- function handleAddNewAttributeChange() {
2954
- setErrorMessage('');
2955
- if (newAttributeKey.trim().length === 0) {
2956
- setErrorMessage('Attribute key is mandatory');
2957
- return;
2958
- }
2959
- if (newAttributeKey === 'Parent') {
2960
- setErrorMessage('"Parent" -key is handled internally and it cannot be modified manually');
2961
- return;
2962
- }
2963
- if (newAttributeKey in attributes) {
2964
- setErrorMessage(`Attribute "${newAttributeKey}" already exists`);
2965
- return;
2966
- }
2967
- if (/^[A-Z]/.test(newAttributeKey) &&
2968
- !reservedTerms$1.includes(newAttributeKey) &&
2969
- ![...reservedKeys$1.keys()].includes(newAttributeKey)) {
2970
- setErrorMessage(`Key cannot starts with uppercase letter unless key is one of these: ${reservedTerms$1.join(', ')}`);
2971
- return;
2972
- }
2973
- setAttributes({ ...attributes, [newAttributeKey]: [] });
2974
- setShowAddNewForm(false);
2975
- setNewAttributeKey('');
2976
- }
2977
- function deleteAttribute(key) {
2978
- setErrorMessage('');
2979
- const { [key]: remove, ...rest } = attributes;
2980
- setAttributes(rest);
2981
- }
2982
- function makeOnChange(id) {
2983
- return (newValue) => {
2984
- setAttributes({ ...attributes, [id]: newValue });
2985
- };
2986
- }
2987
- function handleRadioButtonChange(event, value) {
2988
- if (value === 'custom') {
2989
- setNewAttributeKey('');
2990
- }
2991
- else if (reservedKeys$1.has(value)) {
2992
- setNewAttributeKey(value);
2993
- }
2994
- else {
2995
- setErrorMessage('Unknown attribute type');
2996
- }
2997
- }
2998
- const hasEmptyAttributes = Object.values(attributes).some((value) => value.length === 0 || value.includes(''));
2999
- return (React__default["default"].createElement(Dialog, { open: true, title: "Feature attributes", handleClose: handleClose, maxWidth: false, "data-testid": "modify-feature-attribute" },
3000
- React__default["default"].createElement("form", { onSubmit: onSubmit },
3001
- React__default["default"].createElement(material.DialogContent, null,
3002
- React__default["default"].createElement(material.Grid2, { container: true, direction: "column", spacing: 1 },
3003
- Object.entries(attributes).map(([key, value]) => {
3004
- const EditorComponent = reservedKeys$1.get(key) ?? CustomAttributeValueEditor$1;
3005
- return (React__default["default"].createElement(material.Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
3006
- React__default["default"].createElement(material.Grid2, null,
3007
- React__default["default"].createElement(material.Paper, { variant: "outlined", className: classes.attributeName },
3008
- React__default["default"].createElement(material.Typography, null, key))),
3009
- React__default["default"].createElement(material.Grid2, { flexGrow: 1 },
3010
- React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: makeOnChange(key) })),
3011
- React__default["default"].createElement(material.Grid2, null,
3012
- React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
3013
- deleteAttribute(key);
3014
- } },
3015
- React__default["default"].createElement(DeleteIcon__default["default"], { fontSize: "medium", key: key })))));
3016
- }),
3017
- React__default["default"].createElement(material.Grid2, null,
3018
- React__default["default"].createElement(material.Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
3019
- setShowAddNewForm(true);
3020
- } }, "Add new")),
3021
- showAddNewForm ? (React__default["default"].createElement(material.Grid2, null,
3022
- React__default["default"].createElement(material.Paper, { elevation: 8, className: classes.newAttributePaper },
3023
- React__default["default"].createElement(material.Grid2, { container: true, direction: "column" },
3024
- React__default["default"].createElement(material.Grid2, null,
3025
- React__default["default"].createElement(material.FormControl, null,
3026
- React__default["default"].createElement(material.FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
3027
- React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
3028
- React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid2, { container: true, spacing: 1, alignItems: "center" },
3029
- React__default["default"].createElement(material.Grid2, null,
3030
- React__default["default"].createElement(material.Typography, null, "Custom")),
3031
- React__default["default"].createElement(material.Grid2, null,
3032
- React__default["default"].createElement(material.TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys$1.has(newAttributeKey)
3033
- ? ''
3034
- : newAttributeKey, disabled: reservedKeys$1.has(newAttributeKey), onChange: (event) => {
3035
- setNewAttributeKey(event.target.value);
3036
- } }))) }),
3037
- [...reservedKeys$1.keys()].map((key) => (React__default["default"].createElement(material.FormControlLabel, { key: key, value: key, control: React__default["default"].createElement(material.Radio, null), label: key })))))),
3038
- React__default["default"].createElement(material.Grid2, null,
3039
- React__default["default"].createElement(material.DialogActions, null,
3040
- React__default["default"].createElement(material.Button, { key: "addButton", color: "primary", variant: "contained", style: { margin: 2 }, onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
3041
- React__default["default"].createElement(material.Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
3042
- setShowAddNewForm(false);
3043
- setNewAttributeKey('');
3044
- setErrorMessage('');
3045
- } }, "Cancel"))))))) : null),
3046
- errorMessage ? (React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage)) : null),
3047
- React__default["default"].createElement(material.DialogActions, null,
3048
- React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: showAddNewForm || hasEmptyAttributes || !editable }, "Submit changes"),
3049
- React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", disabled: showAddNewForm, onClick: handleClose }, "Cancel")))));
3050
- }
3051
-
3052
- /* eslint-disable @typescript-eslint/no-unsafe-call */
3053
- function OpenLocalFile({ handleClose, session }) {
3054
- const { apolloDataStore } = session;
3055
- const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
3056
- const [file, setFile] = React.useState(null);
3057
- const [assemblyName, setAssemblyName] = React.useState('');
3058
- const [errorMessage, setErrorMessage] = React.useState('');
3059
- const [submitted, setSubmitted] = React.useState(false);
3060
- const theme = material.useTheme();
3061
- function handleChangeFile(e) {
3062
- const selectedFile = e.target.files?.item(0);
3063
- if (!selectedFile) {
3064
- return;
3065
- }
3066
- setErrorMessage('');
3067
- setFile(selectedFile);
3068
- if (!assemblyName) {
3069
- const fileName = selectedFile.name;
3070
- const lastDotIndex = fileName.lastIndexOf('.');
3071
- if (lastDotIndex === -1) {
3072
- setAssemblyName(fileName);
3073
- }
3074
- else {
3075
- setAssemblyName(fileName.slice(0, lastDotIndex));
3076
- }
3077
- }
3078
- }
3079
- async function onSubmit(event) {
3080
- event.preventDefault();
3081
- setErrorMessage('');
3082
- setSubmitted(true);
3083
- if (!file) {
3084
- throw new Error('No file selected');
2898
+ }
2899
+ async function onSubmit(event) {
2900
+ event.preventDefault();
2901
+ setErrorMessage('');
2902
+ setSubmitted(true);
2903
+ if (!file) {
2904
+ throw new Error('No file selected');
3085
2905
  }
3086
2906
  // Right now we are not using stream because there was a problem with 'pipe' in ReadStream
3087
2907
  const fileData = await new Response(file).text();
@@ -4151,6 +3971,15 @@ class ApolloSequenceAdapter extends BaseAdapter.BaseSequenceAdapter {
4151
3971
  return;
4152
3972
  }
4153
3973
  const backendDriver = dataStore.getBackendDriver(assemblyId);
3974
+ const regions = await backendDriver.getRegions(regionWithAssemblyName.assemblyName);
3975
+ const region = regions.find((region) => region.refName === regionWithAssemblyName.refName);
3976
+ if (!region) {
3977
+ observer.error('Cannot get region');
3978
+ return;
3979
+ }
3980
+ if (regionWithAssemblyName.end > region.end) {
3981
+ regionWithAssemblyName.end = region.end;
3982
+ }
4154
3983
  const { seq } = await backendDriver.getSequence(regionWithAssemblyName);
4155
3984
  observer.next(new SimpleFeature__default["default"]({
4156
3985
  id: `${refName} ${start}-${end}`,
@@ -5019,16 +4848,52 @@ const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
5019
4848
  throw new Error('featureTypeOntology is undefined');
5020
4849
  }
5021
4850
  return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
5022
- featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
5023
- featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
4851
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript') ||
4852
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogene') ||
4853
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogenic_transcript'));
4854
+ };
4855
+ const isGene = (annotationFeature, apolloSessionModel) => {
4856
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4857
+ if (!featureTypeOntology) {
4858
+ throw new Error('featureTypeOntology is undefined');
4859
+ }
4860
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
4861
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogene'));
5024
4862
  };
5025
4863
  const isTranscript = (annotationFeature, apolloSessionModel) => {
5026
4864
  const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
5027
4865
  if (!featureTypeOntology) {
5028
4866
  throw new Error('featureTypeOntology is undefined');
5029
4867
  }
5030
- return (featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
5031
- featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
4868
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript') ||
4869
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogenic_transcript'));
4870
+ };
4871
+ const getFeatureId = (feature) => {
4872
+ const { attributes } = feature;
4873
+ const id = attributes?.id;
4874
+ if (id) {
4875
+ return id[0];
4876
+ }
4877
+ return feature.type;
4878
+ };
4879
+ const getFeatureNameOrId = (feature, apolloSessionModel) => {
4880
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4881
+ if (!featureTypeOntology) {
4882
+ return getFeatureId(feature);
4883
+ }
4884
+ let attrName = '';
4885
+ if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
4886
+ attrName = 'gene_name';
4887
+ }
4888
+ if (featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
4889
+ attrName = 'transcript_name';
4890
+ }
4891
+ const { attributes } = feature;
4892
+ const name = attributes?.[attrName];
4893
+ if (name) {
4894
+ return name[0];
4895
+ }
4896
+ return getFeatureId(feature);
5032
4897
  };
5033
4898
  function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
5034
4899
  const apolloSessionModel = session;
@@ -5053,6 +4918,9 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5053
4918
  const getFeatures = (min, max) => {
5054
4919
  const filteredFeatures = [];
5055
4920
  for (const [, f] of features) {
4921
+ if (f.type === 'chromosome') {
4922
+ continue;
4923
+ }
5056
4924
  const featureSnapshot = mobxStateTree.getSnapshot(f);
5057
4925
  if (min >= featureSnapshot.min && max <= featureSnapshot.max) {
5058
4926
  filteredFeatures.push(featureSnapshot);
@@ -5062,27 +4930,27 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5062
4930
  };
5063
4931
  React.useEffect(() => {
5064
4932
  setErrorMessage('');
5065
- if (checkedChildrens.length === 0) {
5066
- setParentFeatureChecked(false);
5067
- return;
5068
- }
4933
+ let mins = [];
4934
+ let maxes = [];
5069
4935
  if (annotationFeature.children) {
5070
4936
  const checkedAnnotationFeatureChildren = Object.values(annotationFeature.children)
5071
4937
  .filter((child) => isTranscript(child, apolloSessionModel))
5072
4938
  .filter((child) => checkedChildrens.includes(child._id));
5073
- const mins = checkedAnnotationFeatureChildren.map((f) => f.min);
5074
- const maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
5075
- const min = Math.min(...mins);
5076
- const max = Math.max(...maxes);
5077
- const filteredFeatures = getFeatures(min, max);
5078
- setDestinationFeatures(filteredFeatures);
5079
- if (filteredFeatures.length === 0 &&
5080
- checkedChildrens.length > 0 &&
5081
- !parentFeatureChecked) {
5082
- setErrorMessage('No destination features found');
5083
- }
5084
- }
5085
- }, [checkedChildrens]);
4939
+ mins = checkedAnnotationFeatureChildren.map((f) => f.min);
4940
+ maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
4941
+ }
4942
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4943
+ if (featureTypeOntology &&
4944
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript')) {
4945
+ mins = [annotationFeature.min, ...mins];
4946
+ maxes = [annotationFeature.max, ...maxes];
4947
+ }
4948
+ const min = Math.min(...mins);
4949
+ const max = Math.max(...maxes);
4950
+ const filteredFeatures = getFeatures(min, max);
4951
+ setDestinationFeatures(filteredFeatures);
4952
+ setSelectedDestinationFeature(filteredFeatures[0]);
4953
+ }, [checkedChildrens, parentFeatureChecked]);
5086
4954
  const handleParentFeatureCheck = (event) => {
5087
4955
  const isChecked = event.target.checked;
5088
4956
  setParentFeatureChecked(isChecked);
@@ -5099,12 +4967,52 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5099
4967
  };
5100
4968
  const handleCreateApolloAnnotation = async () => {
5101
4969
  if (parentFeatureChecked) {
5102
- const change = new shared.AddFeatureChange({
5103
- changedIds: [annotationFeature._id],
5104
- typeName: 'AddFeatureChange',
5105
- assembly: assembly.name,
5106
- addedFeature: annotationFeature,
5107
- });
4970
+ let change;
4971
+ if (isGene(annotationFeature, apolloSessionModel)) {
4972
+ if (annotationFeature.children &&
4973
+ checkedChildrens.length !==
4974
+ Object.values(annotationFeature.children).length) {
4975
+ const childrens = {};
4976
+ for (const childId of checkedChildrens) {
4977
+ childrens[childId] = annotationFeature.children[childId];
4978
+ }
4979
+ change = new shared.AddFeatureChange({
4980
+ changedIds: [annotationFeature._id],
4981
+ typeName: 'AddFeatureChange',
4982
+ assembly: assembly.name,
4983
+ addedFeature: {
4984
+ ...annotationFeature,
4985
+ children: childrens,
4986
+ },
4987
+ });
4988
+ }
4989
+ else {
4990
+ change = new shared.AddFeatureChange({
4991
+ changedIds: [annotationFeature._id],
4992
+ typeName: 'AddFeatureChange',
4993
+ assembly: assembly.name,
4994
+ addedFeature: annotationFeature,
4995
+ });
4996
+ }
4997
+ }
4998
+ if (isTranscript(annotationFeature, apolloSessionModel)) {
4999
+ if (selectedDestinationFeature) {
5000
+ change = new shared.AddFeatureChange({
5001
+ parentFeatureId: selectedDestinationFeature._id,
5002
+ changedIds: [selectedDestinationFeature._id],
5003
+ typeName: 'AddFeatureChange',
5004
+ assembly: assembly.name,
5005
+ addedFeature: annotationFeature,
5006
+ });
5007
+ }
5008
+ else {
5009
+ setErrorMessage('There is no destination gene for this transcript');
5010
+ return;
5011
+ }
5012
+ }
5013
+ if (!change) {
5014
+ return;
5015
+ }
5108
5016
  await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5109
5017
  session.notify('Annotation added successfully', 'success');
5110
5018
  handleClose();
@@ -5126,27 +5034,28 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5126
5034
  addedFeature: child,
5127
5035
  });
5128
5036
  await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5129
- session.notify('Annotation added successfully', 'success');
5130
- handleClose();
5131
5037
  }
5038
+ session.notify('Annotation added successfully', 'success');
5039
+ handleClose();
5132
5040
  }
5133
5041
  };
5134
5042
  return (React__default["default"].createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
5135
5043
  React__default["default"].createElement(material.DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
5136
5044
  React__default["default"].createElement(material.DialogContent, null,
5137
5045
  React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
5138
- isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${annotationFeature.type}:${annotationFeature.min}..${annotationFeature.max}` })),
5046
+ isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${getFeatureNameOrId(annotationFeature, apolloSessionModel)} (${annotationFeature.min + 1}..${annotationFeature.max})` })),
5139
5047
  annotationFeature.children && (React__default["default"].createElement(material.Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
5140
5048
  .filter((child) => isTranscript(child, apolloSessionModel))
5141
5049
  .map((child) => (React__default["default"].createElement(material.FormControlLabel, { key: child._id, control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
5142
5050
  handleChildFeatureCheck(e, child);
5143
- } }), label: `${child.type}:${child.min}..${child.max}` })))))),
5144
- !parentFeatureChecked &&
5145
- checkedChildrens.length > 0 &&
5146
- destinationFeatures.length > 0 && (React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
5051
+ } }), label: `${getFeatureNameOrId(child, apolloSessionModel)} (${child.min + 1}..${child.max})` })))))),
5052
+ destinationFeatures.length > 0 &&
5053
+ ((!parentFeatureChecked && checkedChildrens.length > 0) ||
5054
+ (parentFeatureChecked &&
5055
+ isTranscript(annotationFeature, apolloSessionModel))) && (React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
5147
5056
  React__default["default"].createElement(material.Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
5148
5057
  React__default["default"].createElement(material.Box, { sx: { mt: 1 } },
5149
- React__default["default"].createElement(material.Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default["default"].createElement(material.MenuItem, { key: f._id, value: f._id }, `${f.type}:${f.min}..${f.max}`)))))))),
5058
+ React__default["default"].createElement(material.Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default["default"].createElement(material.MenuItem, { key: f._id, value: f._id }, `${getFeatureNameOrId(f, apolloSessionModel)} (${f.min}..${f.max})`)))))))),
5150
5059
  React__default["default"].createElement(material.DialogActions, null,
5151
5060
  React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
5152
5061
  (!parentFeatureChecked &&
@@ -5158,6 +5067,7 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5158
5067
  }
5159
5068
 
5160
5069
  function simpleFeatureToGFF3Feature(feature, refSeqId) {
5070
+ // eslint-disable-next-line unicorn/prefer-structured-clone
5161
5071
  const xfeature = JSON.parse(JSON.stringify(feature));
5162
5072
  const children = xfeature.subfeatures;
5163
5073
  const gff3Feature = [
@@ -5202,93 +5112,262 @@ function convertFeatureAttributes(feature) {
5202
5112
  if (defaultFields.has(key)) {
5203
5113
  continue;
5204
5114
  }
5205
- attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
5206
- }
5207
- return attributes;
5208
- }
5209
- function annotationFromJBrowseFeature(pluggableElement) {
5210
- if (pluggableElement.name !== 'LinearBasicDisplay') {
5211
- return pluggableElement;
5212
- }
5213
- const { stateModel } = pluggableElement;
5214
- const newStateModel = stateModel
5215
- .views((self) => ({
5216
- getFirstRegion() {
5217
- const lgv = util.getContainingView(self);
5218
- return lgv.dynamicBlocks.contentBlocks[0];
5219
- },
5220
- getAssembly() {
5221
- const firstRegion = self.getFirstRegion();
5222
- const session = util.getSession(self);
5223
- const { assemblyManager } = session;
5224
- const { assemblyName } = firstRegion;
5225
- const assembly = assemblyManager.get(assemblyName);
5226
- if (!assembly) {
5227
- throw new Error(`Could not find assembly named ${assemblyName}`);
5115
+ attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
5116
+ }
5117
+ return attributes;
5118
+ }
5119
+ function annotationFromJBrowseFeature(pluggableElement) {
5120
+ if (pluggableElement.name !== 'LinearBasicDisplay') {
5121
+ return pluggableElement;
5122
+ }
5123
+ const { stateModel } = pluggableElement;
5124
+ const newStateModel = stateModel
5125
+ .views((self) => ({
5126
+ getFirstRegion() {
5127
+ const lgv = util.getContainingView(self);
5128
+ return lgv.dynamicBlocks.contentBlocks[0];
5129
+ },
5130
+ getAssembly() {
5131
+ const firstRegion = self.getFirstRegion();
5132
+ const session = util.getSession(self);
5133
+ const { assemblyManager } = session;
5134
+ const { assemblyName } = firstRegion;
5135
+ const assembly = assemblyManager.get(assemblyName);
5136
+ if (!assembly) {
5137
+ throw new Error(`Could not find assembly named ${assemblyName}`);
5138
+ }
5139
+ return assembly;
5140
+ },
5141
+ getRefSeqId(assembly) {
5142
+ const firstRegion = self.getFirstRegion();
5143
+ const { refName } = firstRegion;
5144
+ const { refNameAliases } = assembly;
5145
+ if (!refNameAliases) {
5146
+ throw new Error(`Could not find aliases for ${assembly.name}`);
5147
+ }
5148
+ const newRefNames = [...Object.entries(refNameAliases)]
5149
+ .filter(([id, refName]) => id !== refName)
5150
+ .map(([id, refName]) => ({
5151
+ _id: id,
5152
+ name: refName,
5153
+ }));
5154
+ const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
5155
+ if (!refSeqId) {
5156
+ throw new Error(`Could not find refSeqId named ${refName}`);
5157
+ }
5158
+ return refSeqId;
5159
+ },
5160
+ getAnnotationFeature(assembly) {
5161
+ const refSeqId = self.getRefSeqId(assembly);
5162
+ const sfeature = self.contextMenuFeature.data;
5163
+ return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
5164
+ },
5165
+ }))
5166
+ .views((self) => {
5167
+ const superContextMenuItems = self.contextMenuItems;
5168
+ return {
5169
+ contextMenuItems() {
5170
+ const session = util.getSession(self);
5171
+ const assembly = self.getAssembly();
5172
+ const feature = self.contextMenuFeature;
5173
+ if (!feature) {
5174
+ return superContextMenuItems();
5175
+ }
5176
+ return [
5177
+ ...superContextMenuItems(),
5178
+ {
5179
+ label: 'Create Apollo annotation',
5180
+ icon: AddIcon__default["default"],
5181
+ onClick: () => {
5182
+ session.queueDialog((doneCallback) => [
5183
+ CreateApolloAnnotation,
5184
+ {
5185
+ session,
5186
+ handleClose: () => {
5187
+ doneCallback();
5188
+ },
5189
+ annotationFeature: self.getAnnotationFeature(assembly),
5190
+ assembly,
5191
+ refSeqId: self.getRefSeqId(assembly),
5192
+ },
5193
+ ]);
5194
+ },
5195
+ },
5196
+ ];
5197
+ },
5198
+ };
5199
+ });
5200
+ pluggableElement.stateModel = newStateModel;
5201
+ return pluggableElement;
5202
+ }
5203
+
5204
+ /* eslint-disable @typescript-eslint/unbound-method */
5205
+ // interface TermAutocompleteResult extends TermValue {
5206
+ // label: string[]
5207
+ // match: string
5208
+ // category: string[]
5209
+ // taxon: string
5210
+ // taxon_label: string
5211
+ // highlight: string
5212
+ // has_highlight: boolean
5213
+ // }
5214
+ // interface TermAutocompleteResponse {
5215
+ // docs: TermAutocompleteResult[]
5216
+ // }
5217
+ // const hiliteRegex = /(?<=<em class="hilite">)(.*?)(?=<\/em>)/g
5218
+ function TermTagWithTooltip({ getTagProps, index, ontology, termId, }) {
5219
+ const manager = mobxStateTree.getParent(ontology, 2);
5220
+ const [description, setDescription] = React__namespace.useState('');
5221
+ const [errorMessage, setErrorMessage] = React__namespace.useState('');
5222
+ React__namespace.useEffect(() => {
5223
+ const controller = new AbortController();
5224
+ const { signal } = controller;
5225
+ async function fetchDescription() {
5226
+ const termUrl = manager.expandPrefixes(termId);
5227
+ const db = await ontology.dataStore?.db;
5228
+ if (!db || signal.aborted) {
5229
+ return;
5230
+ }
5231
+ const term = await db
5232
+ .transaction('nodes')
5233
+ .objectStore('nodes')
5234
+ .get(termUrl);
5235
+ if (term && term.lbl && !signal.aborted) {
5236
+ setDescription(term.lbl || 'no label');
5237
+ }
5238
+ }
5239
+ fetchDescription().catch((error) => {
5240
+ if (!signal.aborted) {
5241
+ setErrorMessage(String(error));
5242
+ }
5243
+ });
5244
+ return () => {
5245
+ controller.abort();
5246
+ };
5247
+ }, [termId, ontology, manager]);
5248
+ return (React__namespace.createElement(material.Tooltip, { title: description },
5249
+ React__namespace.createElement("div", null,
5250
+ React__namespace.createElement(material.Chip, { label: errorMessage || manager.applyPrefixes(termId), color: errorMessage ? 'error' : 'default', size: "small", ...getTagProps({ index }) }))));
5251
+ }
5252
+ function OntologyTermMultiSelect({ includeDeprecated, onChange, ontologyName, ontologyVersion, session, value: initialValue, label, }) {
5253
+ const { ontologyManager } = session.apolloDataStore;
5254
+ const ontology = ontologyManager.findOntology(ontologyName, ontologyVersion);
5255
+ const [value, setValue] = React__namespace.useState(initialValue.map((id) => ({ term: { id, type: 'CLASS' } })));
5256
+ const [inputValue, setInputValue] = React__namespace.useState('');
5257
+ const [options, setOptions] = React__namespace.useState([]);
5258
+ const [loading, setLoading] = React__namespace.useState(false);
5259
+ const [errorMessage, setErrorMessage] = React__namespace.useState('');
5260
+ const getOntologyTerms = React__namespace.useMemo(() => utils.debounce(async (request, callback) => {
5261
+ if (!ontology) {
5262
+ return;
5263
+ }
5264
+ const { dataStore } = ontology;
5265
+ if (!dataStore) {
5266
+ return;
5267
+ }
5268
+ const { input, signal } = request;
5269
+ try {
5270
+ const matches = await dataStore.getTermsByFulltext(input, undefined, signal);
5271
+ // aggregate the matches by term
5272
+ const byTerm = new Map();
5273
+ const options = [];
5274
+ for (const match of matches) {
5275
+ if (!isOntologyClass(match.term) ||
5276
+ (!includeDeprecated && isDeprecated(match.term))) {
5277
+ continue;
5278
+ }
5279
+ let slot = byTerm.get(match.term.id);
5280
+ if (!slot) {
5281
+ slot = { term: match.term, matches: [] };
5282
+ byTerm.set(match.term.id, slot);
5283
+ options.push(slot);
5284
+ }
5285
+ slot.matches.push(match);
5286
+ }
5287
+ callback(options);
5288
+ }
5289
+ catch (error) {
5290
+ if (!aborting.isAbortException(error)) {
5291
+ setErrorMessage(String(error));
5228
5292
  }
5229
- return assembly;
5230
- },
5231
- getRefSeqId(assembly) {
5232
- const firstRegion = self.getFirstRegion();
5233
- const { refName } = firstRegion;
5234
- const { refNameAliases } = assembly;
5235
- if (!refNameAliases) {
5236
- throw new Error(`Could not find aliases for ${assembly.name}`);
5293
+ }
5294
+ }, 400), [includeDeprecated, ontology]);
5295
+ React__namespace.useEffect(() => {
5296
+ const aborter = new AbortController();
5297
+ const { signal } = aborter;
5298
+ if (inputValue === '') {
5299
+ setOptions([]);
5300
+ return;
5301
+ }
5302
+ setLoading(true);
5303
+ void getOntologyTerms({ input: inputValue, signal }, (results) => {
5304
+ let newOptions = [];
5305
+ if (value.length > 0) {
5306
+ newOptions = value;
5237
5307
  }
5238
- const newRefNames = [...Object.entries(refNameAliases)]
5239
- .filter(([id, refName]) => id !== refName)
5240
- .map(([id, refName]) => ({
5241
- _id: id,
5242
- name: refName,
5243
- }));
5244
- const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
5245
- if (!refSeqId) {
5246
- throw new Error(`Could not find refSeqId named ${refName}`);
5308
+ if (results) {
5309
+ newOptions = [...newOptions, ...results];
5247
5310
  }
5248
- return refSeqId;
5249
- },
5250
- getAnnotationFeature(assembly) {
5251
- const refSeqId = self.getRefSeqId(assembly);
5252
- const sfeature = self.contextMenuFeature.data;
5253
- return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
5254
- },
5255
- }))
5256
- .views((self) => {
5257
- const superContextMenuItems = self.contextMenuItems;
5258
- const session = util.getSession(self);
5259
- const assembly = self.getAssembly();
5260
- return {
5261
- contextMenuItems() {
5262
- const feature = self.contextMenuFeature;
5263
- if (!feature) {
5264
- return superContextMenuItems();
5265
- }
5266
- return [
5267
- ...superContextMenuItems(),
5268
- {
5269
- label: 'Create Apollo annotation',
5270
- icon: AddIcon__default["default"],
5271
- onClick: () => {
5272
- session.queueDialog((doneCallback) => [
5273
- CreateApolloAnnotation,
5274
- {
5275
- session,
5276
- handleClose: () => {
5277
- doneCallback();
5278
- },
5279
- annotationFeature: self.getAnnotationFeature(assembly),
5280
- assembly,
5281
- refSeqId: self.getRefSeqId(assembly),
5282
- },
5283
- ]);
5284
- },
5285
- },
5286
- ];
5287
- },
5311
+ setOptions(newOptions);
5312
+ setLoading(false);
5313
+ });
5314
+ return () => {
5315
+ aborter.abort();
5288
5316
  };
5317
+ }, [getOntologyTerms, ontology, includeDeprecated, inputValue, value]);
5318
+ if (!ontology) {
5319
+ return null;
5320
+ }
5321
+ const extraTextFieldParams = {};
5322
+ if (errorMessage) {
5323
+ extraTextFieldParams.error = true;
5324
+ extraTextFieldParams.helperText = errorMessage;
5325
+ }
5326
+ return (React__namespace.createElement(material.Autocomplete, { getOptionLabel: (option) => ontologyManager.applyPrefixes(option.term.id), filterOptions: (terms) => terms.filter((t) => isOntologyClass(t.term)), options: options, autoComplete: true, includeInputInList: true, filterSelectedOptions: true, value: value, loading: loading, isOptionEqualToValue: (option, v) => ontologyManager.applyPrefixes(option.term.id) ===
5327
+ ontologyManager.applyPrefixes(v.term.id), noOptionsText: inputValue ? 'No matches' : 'Start typing to search', onChange: (_, newValue) => {
5328
+ setOptions(newValue ? [...newValue, ...options] : options);
5329
+ onChange(newValue.map((v) => ontologyManager.applyPrefixes(v.term.id)));
5330
+ setValue(newValue);
5331
+ }, onInputChange: (event, newInputValue) => {
5332
+ if (newInputValue) {
5333
+ setLoading(true);
5334
+ }
5335
+ setOptions([]);
5336
+ setInputValue(newInputValue);
5337
+ }, multiple: true, renderInput: (params) => (React__namespace.createElement(material.TextField, { ...params, ...extraTextFieldParams, variant: "outlined", label: label, fullWidth: true })), renderOption: (props, option) => (React__namespace.createElement(Option, { ...props, ontologyManager: ontologyManager, option: option, inputValue: inputValue })), renderTags: (v, getTagProps) => v.map((option, index) => (React__namespace.createElement(TermTagWithTooltip, { termId: option.term.id, index: index, ontology: ontology, getTagProps: getTagProps, key: option.term.id }))) }));
5338
+ }
5339
+ function HighlightedText(props) {
5340
+ const { search, str } = props;
5341
+ const highlights = highlightMatch__default["default"](str, search, {
5342
+ insideWords: true,
5343
+ findAllOccurrences: true,
5289
5344
  });
5290
- pluggableElement.stateModel = newStateModel;
5291
- return pluggableElement;
5345
+ const parts = highlightParse__default["default"](str, highlights);
5346
+ return (React__namespace.createElement(React__namespace.Fragment, null, parts.map((part, index) => (React__namespace.createElement(material.Typography, { key: index, component: "span", sx: { fontWeight: part.highlight ? 'bold' : 'regular' }, variant: "body2", color: "text.secondary" }, part.text)))));
5347
+ }
5348
+ function Option(props) {
5349
+ const { inputValue, ontologyManager, option, ...other } = props;
5350
+ const matches = option.matches ?? [];
5351
+ const fields = matches
5352
+ .filter((match) => match.field.jsonPath !== '$.lbl')
5353
+ .map((match) => {
5354
+ return (React__namespace.createElement(React__namespace.Fragment, { key: `option-${match.term.id}-${match.str}` },
5355
+ React__namespace.createElement(material.Typography, { component: "dt", variant: "body2", color: "text.secondary" }, match.field.displayName),
5356
+ React__namespace.createElement("dd", null,
5357
+ React__namespace.createElement(HighlightedText, { str: match.str, search: inputValue }))));
5358
+ });
5359
+ // const lblScore = matches
5360
+ // .filter((match) => match.field.jsonPath === '$.lbl')
5361
+ // .map((m) => m.score)
5362
+ // .join(', ')
5363
+ return (React__namespace.createElement("li", { ...other },
5364
+ React__namespace.createElement(material.Grid2, { container: true },
5365
+ React__namespace.createElement(material.Grid2, null,
5366
+ React__namespace.createElement(material.Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
5367
+ ' ',
5368
+ React__namespace.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
5369
+ ' ',
5370
+ React__namespace.createElement("dl", null, fields)))));
5292
5371
  }
5293
5372
 
5294
5373
  /* eslint-disable @typescript-eslint/unbound-method */
@@ -5329,13 +5408,13 @@ const reservedKeys = new Map([
5329
5408
  [
5330
5409
  'Gene Ontology',
5331
5410
  (props) => {
5332
- return React__default["default"].createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Gene Ontology" });
5411
+ return (React__default["default"].createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Gene Ontology", label: 'Gene Ontology' }));
5333
5412
  },
5334
5413
  ],
5335
5414
  [
5336
5415
  'Sequence Ontology',
5337
5416
  (props) => {
5338
- return (React__default["default"].createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Sequence Ontology" }));
5417
+ return (React__default["default"].createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Sequence Ontology", label: 'Sequence Ontology' }));
5339
5418
  },
5340
5419
  ],
5341
5420
  ]);
@@ -5362,17 +5441,13 @@ const useStyles$a = mui.makeStyles()((theme) => ({
5362
5441
  },
5363
5442
  }));
5364
5443
  function CustomAttributeValueEditor(props) {
5365
- const { onChange, value } = props;
5444
+ const { onChange, value, label } = props;
5366
5445
  return (React__default["default"].createElement(StringTextField, { value: value, onChangeCommitted: (newValue) => {
5367
5446
  onChange(newValue.split(','));
5368
- }, variant: "outlined", fullWidth: true, helperText: "Separate multiple values for the attribute with commas" }));
5447
+ }, variant: "outlined", fullWidth: true, label: label, style: { width: '100%' } }));
5369
5448
  }
5370
- const Attributes = mobxReact.observer(function Attributes({ assembly, editable, feature, session, }) {
5371
- const [errorMessage, setErrorMessage] = React.useState('');
5372
- const [showAddNewForm, setShowAddNewForm] = React.useState(false);
5373
- const { classes } = useStyles$a();
5374
- const [newAttributeKey, setNewAttributeKey] = React.useState('');
5375
- const attributes = Object.fromEntries([...feature.attributes.entries()].map(([key, value]) => {
5449
+ function transformAttributes(feature) {
5450
+ return Object.fromEntries([...feature.attributes.entries()].map(([key, value]) => {
5376
5451
  if (key.startsWith('gff_')) {
5377
5452
  const newKey = key.slice(4);
5378
5453
  const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1);
@@ -5383,17 +5458,23 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
5383
5458
  }
5384
5459
  return [key, mobxStateTree.getSnapshot(value)];
5385
5460
  }));
5461
+ }
5462
+ const Attributes = mobxReact.observer(function Attributes({ assembly, editable, feature, session, }) {
5463
+ const [errorMessage, setErrorMessage] = React.useState('');
5464
+ const [showAddNewForm, setShowAddNewForm] = React.useState(false);
5465
+ const { classes } = useStyles$a();
5466
+ const [newAttributeKey, setNewAttributeKey] = React.useState('');
5467
+ const [attributes, setAttributes] = React.useState(() => transformAttributes(feature));
5468
+ React.useEffect(() => {
5469
+ setAttributes(transformAttributes(feature));
5470
+ }, [feature]);
5386
5471
  const { notify } = session;
5387
5472
  const { changeManager } = session.apolloDataStore;
5388
- async function onChangeCommitted(newKey, newValue) {
5473
+ async function onChangeCommitted(attributes) {
5389
5474
  setErrorMessage('');
5390
5475
  const attrs = {};
5391
5476
  if (attributes) {
5392
- const modifiedAttrs = Object.entries({
5393
- ...attributes,
5394
- [newKey]: newValue,
5395
- });
5396
- for (const [key, val] of modifiedAttrs) {
5477
+ for (const [key, val] of Object.entries(attributes)) {
5397
5478
  if (!val) {
5398
5479
  continue;
5399
5480
  }
@@ -5482,7 +5563,25 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
5482
5563
  setErrorMessage(`Key cannot starts with uppercase letter unless key is one of these: ${reservedTerms.join(', ')}`);
5483
5564
  return;
5484
5565
  }
5485
- void onChangeCommitted(newAttributeKey, []);
5566
+ setAttributes({
5567
+ ...attributes,
5568
+ [newAttributeKey]: [],
5569
+ });
5570
+ setShowAddNewForm(false);
5571
+ setNewAttributeKey('');
5572
+ }
5573
+ function deleteAttribute(key) {
5574
+ const newAttributes = { ...attributes };
5575
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
5576
+ delete newAttributes[key];
5577
+ setAttributes(newAttributes);
5578
+ void onChangeCommitted(newAttributes);
5579
+ }
5580
+ function updateAttribute(key, newValue) {
5581
+ const newAttributes = { ...attributes };
5582
+ newAttributes[key] = newValue;
5583
+ setAttributes(newAttributes);
5584
+ void onChangeCommitted(newAttributes);
5486
5585
  }
5487
5586
  function handleRadioButtonChange(event, value) {
5488
5587
  if (value === 'custom') {
@@ -5495,22 +5594,22 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
5495
5594
  setErrorMessage('Unknown attribute type');
5496
5595
  }
5497
5596
  }
5498
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
5499
- React__default["default"].createElement(material.Typography, { variant: "h5" }, "Attributes"),
5597
+ return (React__default["default"].createElement("div", { "data-testid": "attributes_test" },
5500
5598
  React__default["default"].createElement(material.Grid2, { container: true, direction: "column", spacing: 1 },
5501
5599
  Object.entries(attributes).map(([key, value]) => {
5502
5600
  if (key === '') {
5503
5601
  return null;
5504
5602
  }
5505
5603
  const EditorComponent = reservedKeys.get(key) ?? CustomAttributeValueEditor;
5506
- return (React__default["default"].createElement(material.Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
5507
- React__default["default"].createElement(material.Grid2, null,
5508
- React__default["default"].createElement(material.Paper, { variant: "outlined", className: classes.attributeName },
5509
- React__default["default"].createElement(material.Typography, null, key))),
5510
- React__default["default"].createElement(material.Grid2, { flexGrow: 1 },
5511
- React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => onChangeCommitted(key, newValue) })),
5512
- React__default["default"].createElement(material.Grid2, null,
5513
- React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => onChangeCommitted(key) },
5604
+ return (React__default["default"].createElement(material.Grid2, { container: true, key: key },
5605
+ React__default["default"].createElement(material.Grid2, { size: 11 },
5606
+ React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => {
5607
+ updateAttribute(key, newValue);
5608
+ }, label: key })),
5609
+ React__default["default"].createElement(material.Grid2, { size: 1 },
5610
+ React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
5611
+ deleteAttribute(key);
5612
+ }, style: { marginTop: '10px' } },
5514
5613
  React__default["default"].createElement(DeleteIcon__default["default"], { fontSize: "medium", key: key })))));
5515
5614
  }),
5516
5615
  React__default["default"].createElement(material.Grid2, null,
@@ -5651,8 +5750,7 @@ const BasicInformation = mobxReact.observer(function BasicInformation({ assembly
5651
5750
  }
5652
5751
  return terms;
5653
5752
  }
5654
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
5655
- React__default["default"].createElement(material.Typography, { variant: "h5" }, "Basic information"),
5753
+ return (React__default["default"].createElement("div", { "data-testid": "basic_information" },
5656
5754
  React__default["default"].createElement(NumberTextField, { margin: "dense", id: "start", label: "Start", fullWidth: true, variant: "outlined", value: min + 1, onChangeCommitted: handleStartChange }),
5657
5755
  React__default["default"].createElement(NumberTextField, { margin: "dense", id: "end", label: "End", fullWidth: true, variant: "outlined", value: max, onChangeCommitted: handleEndChange }),
5658
5756
  React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTerms.bind(null, feature), renderInput: (params) => (React__default["default"].createElement(material.TextField, { ...params, label: "Type", variant: "outlined", fullWidth: true, error: Boolean(typeWarningText), helperText: typeWarningText })), onChange: (oldValue, newValue) => {
@@ -5685,11 +5783,7 @@ const useStyles$9 = mui.makeStyles()({
5685
5783
  });
5686
5784
  const Sequence = mobxReact.observer(function Sequence({ assembly, feature, refName, session, }) {
5687
5785
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5688
- const [showSequence, setShowSequence] = React.useState(false);
5689
5786
  const { classes } = useStyles$9();
5690
- const onButtonClick = () => {
5691
- setShowSequence(!showSequence);
5692
- };
5693
5787
  if (!(feature && currentAssembly)) {
5694
5788
  return null;
5695
5789
  }
@@ -5698,22 +5792,17 @@ const Sequence = mobxReact.observer(function Sequence({ assembly, feature, refNa
5698
5792
  return null;
5699
5793
  }
5700
5794
  const { max, min } = feature;
5701
- let sequence = '';
5702
- if (showSequence) {
5703
- sequence = refSeq.getSequence(min, max);
5704
- if (sequence) {
5705
- sequence = formatSequence(sequence, refName, min, max);
5706
- }
5707
- else {
5708
- void session.apolloDataStore.loadRefSeq([
5709
- { assemblyName: assembly, refName, start: min, end: max },
5710
- ]);
5711
- }
5795
+ let sequence = refSeq.getSequence(min, max);
5796
+ if (sequence) {
5797
+ sequence = formatSequence(sequence, refName, min, max);
5712
5798
  }
5713
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
5714
- React__default["default"].createElement(material.Typography, { variant: "h5" }, "Sequence"),
5715
- React__default["default"].createElement(material.Button, { variant: "contained", onClick: onButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence'),
5716
- React__default["default"].createElement("div", null, showSequence && (React__default["default"].createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })))));
5799
+ else {
5800
+ void session.apolloDataStore.loadRefSeq([
5801
+ { assemblyName: assembly, refName, start: min, end: max },
5802
+ ]);
5803
+ }
5804
+ return (React__default["default"].createElement("div", null,
5805
+ React__default["default"].createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })));
5717
5806
  });
5718
5807
 
5719
5808
  const FeatureDetailsNavigation = mobxReact.observer(function FeatureDetailsNavigation(props) {
@@ -5728,14 +5817,14 @@ const FeatureDetailsNavigation = mobxReact.observer(function FeatureDetailsNavig
5728
5817
  if (!(parent ?? childFeatures.length > 0)) {
5729
5818
  return null;
5730
5819
  }
5731
- return (React__default["default"].createElement("div", null,
5732
- React__default["default"].createElement(material.Typography, { variant: "h5" }, "Go to related feature"),
5820
+ return (React__default["default"].createElement("div", { style: { marginTop: 10 } },
5733
5821
  parent && (React__default["default"].createElement("div", null,
5734
5822
  React__default["default"].createElement(material.Typography, { variant: "h6" }, "Parent:"),
5735
5823
  React__default["default"].createElement(material.Button, { variant: "contained", onClick: () => {
5736
5824
  model.setFeature(parent);
5737
5825
  } },
5738
5826
  parent.type,
5827
+ getFeatureNameOrId$1(parent),
5739
5828
  " (",
5740
5829
  parent.min,
5741
5830
  "..",
@@ -5750,6 +5839,7 @@ const FeatureDetailsNavigation = mobxReact.observer(function FeatureDetailsNavig
5750
5839
  model.setFeature(child);
5751
5840
  } },
5752
5841
  child.type,
5842
+ getFeatureNameOrId$1(child),
5753
5843
  " (",
5754
5844
  child.min,
5755
5845
  "..",
@@ -5768,6 +5858,10 @@ const ApolloFeatureDetailsWidget = mobxReact.observer(function ApolloFeatureDeta
5768
5858
  const session = util.getSession(model);
5769
5859
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5770
5860
  const { classes } = useStyles$8();
5861
+ const [panelState, setPanelState] = React.useState(['attributes']);
5862
+ React.useEffect(() => {
5863
+ setPanelState(['attributes']);
5864
+ }, [feature]);
5771
5865
  if (!(feature && currentAssembly)) {
5772
5866
  return null;
5773
5867
  }
@@ -5782,14 +5876,36 @@ const ApolloFeatureDetailsWidget = mobxReact.observer(function ApolloFeatureDeta
5782
5876
  { assemblyName: assembly, refName, start: min, end: max },
5783
5877
  ]);
5784
5878
  }
5879
+ function handlePanelChange(expanded, panel) {
5880
+ if (expanded) {
5881
+ setPanelState([...panelState, panel]);
5882
+ }
5883
+ else {
5884
+ setPanelState(panelState.filter((p) => p !== panel));
5885
+ }
5886
+ }
5785
5887
  return (React__default["default"].createElement("div", { className: classes.root },
5786
5888
  React__default["default"].createElement(BasicInformation, { feature: feature, session: session, assembly: currentAssembly._id }),
5787
- React__default["default"].createElement("hr", null),
5788
- React__default["default"].createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }),
5789
- React__default["default"].createElement("hr", null),
5790
- React__default["default"].createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName }),
5791
- React__default["default"].createElement("hr", null),
5792
- React__default["default"].createElement(FeatureDetailsNavigation, { model: model, feature: feature })));
5889
+ React__default["default"].createElement(material.Accordion, { style: { marginTop: 10 }, expanded: panelState.includes('attributes'), onChange: (e, expanded) => {
5890
+ handlePanelChange(expanded, 'attributes');
5891
+ } },
5892
+ React__default["default"].createElement(material.AccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
5893
+ React__default["default"].createElement(material.Typography, { component: "span" }, "Attributes")),
5894
+ React__default["default"].createElement(material.AccordionDetails, null,
5895
+ React__default["default"].createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }))),
5896
+ React__default["default"].createElement(material.Accordion, { style: { marginTop: 10 }, expanded: panelState.includes('sequence'), onChange: (e, expanded) => {
5897
+ handlePanelChange(expanded, 'sequence');
5898
+ } },
5899
+ React__default["default"].createElement(material.AccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel2-content", id: "panel2-header" },
5900
+ React__default["default"].createElement(material.Typography, { component: "span" }, "Sequence")),
5901
+ React__default["default"].createElement(material.AccordionDetails, null, panelState.includes('sequence') && (React__default["default"].createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName })))),
5902
+ React__default["default"].createElement(material.Accordion, { style: { marginTop: 10 }, expanded: panelState.includes('related_features'), onChange: (e, expanded) => {
5903
+ handlePanelChange(expanded, 'related_features');
5904
+ } },
5905
+ React__default["default"].createElement(material.AccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel3-content", id: "panel3-header" },
5906
+ React__default["default"].createElement(material.Typography, { component: "span" }, "Related features")),
5907
+ React__default["default"].createElement(material.AccordionDetails, null,
5908
+ React__default["default"].createElement(FeatureDetailsNavigation, { model: model, feature: feature })))));
5793
5909
  });
5794
5910
 
5795
5911
  /* eslint-disable @typescript-eslint/no-unsafe-call */
@@ -5857,154 +5973,32 @@ const ApolloTranscriptDetailsModel = mobxStateTree.types
5857
5973
  .actions((self) => ({
5858
5974
  setFeature(feature) {
5859
5975
  // @ts-expect-error Not sure why TS thinks these MST types don't match
5860
- self.feature = feature;
5861
- },
5862
- setTryReload(featureId) {
5863
- self.tryReload = featureId;
5864
- },
5865
- }))
5866
- .actions((self) => ({
5867
- afterAttach() {
5868
- mobxStateTree.addDisposer(self, mobx.autorun((reaction) => {
5869
- if (!self.tryReload) {
5870
- return;
5871
- }
5872
- const session = util.getSession(self);
5873
- const { apolloDataStore } = session;
5874
- if (!apolloDataStore) {
5875
- return;
5876
- }
5877
- const feature = apolloDataStore.getFeature(self.tryReload);
5878
- if (feature) {
5879
- self.setFeature(feature);
5880
- self.setTryReload();
5881
- reaction.dispose();
5882
- }
5883
- }));
5884
- },
5885
- }));
5886
-
5887
- const TranscriptBasicInformation = mobxReact.observer(function TranscriptBasicInformation({ assembly, feature, refName, session, }) {
5888
- const { notify } = session;
5889
- const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5890
- const refData = currentAssembly?.getByRefName(refName);
5891
- const { changeManager } = session.apolloDataStore;
5892
- const theme = material.useTheme();
5893
- function handleLocationChange(oldLocation, newLocation, feature, isMin) {
5894
- if (!feature.children) {
5895
- throw new Error('Transcript should have child features');
5896
- }
5897
- for (const [, child] of feature.children) {
5898
- if (isMin && oldLocation - 1 === child.min) {
5899
- const change = new shared.LocationStartChange({
5900
- typeName: 'LocationStartChange',
5901
- changedIds: [child._id],
5902
- featureId: feature._id,
5903
- oldStart: oldLocation - 1,
5904
- newStart: newLocation - 1,
5905
- assembly,
5906
- });
5907
- changeManager.submit(change).catch(() => {
5908
- notify('Error updating feature start position', 'error');
5909
- });
5910
- return;
5911
- }
5912
- if (!isMin && newLocation === child.max) {
5913
- const change = new shared.LocationEndChange({
5914
- typeName: 'LocationEndChange',
5915
- changedIds: [child._id],
5916
- featureId: feature._id,
5917
- oldEnd: child.max,
5918
- newEnd: newLocation,
5919
- assembly,
5920
- });
5921
- changeManager.submit(change).catch(() => {
5922
- notify('Error updating feature start position', 'error');
5923
- });
5976
+ self.feature = feature;
5977
+ },
5978
+ setTryReload(featureId) {
5979
+ self.tryReload = featureId;
5980
+ },
5981
+ }))
5982
+ .actions((self) => ({
5983
+ afterAttach() {
5984
+ mobxStateTree.addDisposer(self, mobx.autorun((reaction) => {
5985
+ if (!self.tryReload) {
5924
5986
  return;
5925
5987
  }
5926
- }
5927
- }
5928
- if (!refData) {
5929
- return null;
5930
- }
5931
- let strand, transcriptParts;
5932
- try {
5933
- ;
5934
- ({ strand, transcriptParts } = feature);
5935
- }
5936
- catch {
5937
- return null;
5938
- }
5939
- const [firstLocation] = transcriptParts;
5940
- const locationData = firstLocation
5941
- .map((loc, idx) => {
5942
- const { max, min, type } = loc;
5943
- let label = type;
5944
- if (label === 'threePrimeUTR') {
5945
- label = '3` UTR';
5946
- }
5947
- else if (label === 'fivePrimeUTR') {
5948
- label = '5` UTR';
5949
- }
5950
- let fivePrimeSpliceSite;
5951
- let threePrimeSpliceSite;
5952
- let frameColor;
5953
- if (type === 'CDS') {
5954
- const { phase } = loc;
5955
- const frame = util.getFrame(min, max, strand ?? 1, phase);
5956
- frameColor = theme.palette.framesCDS.at(frame)?.main;
5957
- const previousLoc = firstLocation.at(idx - 1);
5958
- const nextLoc = firstLocation.at(idx + 1);
5959
- if (strand === 1) {
5960
- if (previousLoc?.type === 'intron') {
5961
- fivePrimeSpliceSite = refData.getSequence(min - 2, min);
5962
- }
5963
- if (nextLoc?.type === 'intron') {
5964
- threePrimeSpliceSite = refData.getSequence(max, max + 2);
5965
- }
5988
+ const session = util.getSession(self);
5989
+ const { apolloDataStore } = session;
5990
+ if (!apolloDataStore) {
5991
+ return;
5966
5992
  }
5967
- else {
5968
- if (previousLoc?.type === 'intron') {
5969
- fivePrimeSpliceSite = util.revcom(refData.getSequence(max, max + 2));
5970
- }
5971
- if (nextLoc?.type === 'intron') {
5972
- threePrimeSpliceSite = util.revcom(refData.getSequence(min - 2, min));
5973
- }
5993
+ const feature = apolloDataStore.getFeature(self.tryReload);
5994
+ if (feature) {
5995
+ self.setFeature(feature);
5996
+ self.setTryReload();
5997
+ reaction.dispose();
5974
5998
  }
5975
- }
5976
- return {
5977
- min,
5978
- max,
5979
- label,
5980
- fivePrimeSpliceSite,
5981
- threePrimeSpliceSite,
5982
- frameColor,
5983
- };
5984
- })
5985
- .filter((loc) => loc.label !== 'intron');
5986
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
5987
- React__default["default"].createElement(material.Typography, { variant: "h5" }, "Structure"),
5988
- React__default["default"].createElement(material.Typography, { variant: "h6" },
5989
- strand === 1 ? 'Forward' : 'Reverse',
5990
- " strand"),
5991
- React__default["default"].createElement(material.TableContainer, { component: material.Paper },
5992
- React__default["default"].createElement(material.Table, { size: "small" },
5993
- React__default["default"].createElement(material.TableBody, null, locationData.map((loc) => (React__default["default"].createElement(material.TableRow, { key: `${loc.label}:${loc.min}-${loc.max}` },
5994
- React__default["default"].createElement(material.TableCell, { component: "th", scope: "row", style: { background: loc.frameColor } }, loc.label),
5995
- React__default["default"].createElement(material.TableCell, null, loc.fivePrimeSpliceSite ?? ''),
5996
- React__default["default"].createElement(material.TableCell, { padding: "none" },
5997
- React__default["default"].createElement(NumberTextField, { margin: "dense", variant: "outlined", value: strand === 1 ? loc.min + 1 : loc.max, onChangeCommitted: (newLocation) => {
5998
- handleLocationChange(strand === 1 ? loc.min + 1 : loc.max, newLocation, feature, strand === 1);
5999
- } })),
6000
- React__default["default"].createElement(material.TableCell, { padding: "none" },
6001
- React__default["default"].createElement(NumberTextField, { margin: "dense",
6002
- // disabled={item.type !== 'CDS'}
6003
- variant: "outlined", value: strand === 1 ? loc.max : loc.min + 1, onChangeCommitted: (newLocation) => {
6004
- handleLocationChange(strand === 1 ? loc.max : loc.min + 1, newLocation, feature, strand !== 1);
6005
- } })),
6006
- React__default["default"].createElement(material.TableCell, null, loc.threePrimeSpliceSite ?? '')))))))));
6007
- });
5999
+ }));
6000
+ },
6001
+ }));
6008
6002
 
6009
6003
  const SEQUENCE_WRAP_LENGTH = 60;
6010
6004
  function getSequenceSegments(segmentType, feature, getSequence) {
@@ -6105,6 +6099,7 @@ function getSegmentColor(type) {
6105
6099
  case 'upOrDownstream': {
6106
6100
  return 'rgb(255,255,255)';
6107
6101
  }
6102
+ case 'exon':
6108
6103
  case 'UTR': {
6109
6104
  return 'rgb(194,106,119)';
6110
6105
  }
@@ -6119,13 +6114,54 @@ function getSegmentColor(type) {
6119
6114
  }
6120
6115
  }
6121
6116
  }
6117
+ function getLocationIntervals(seqSegments) {
6118
+ const locIntervals = [];
6119
+ const allLocs = seqSegments.flatMap((segment) => segment.locs);
6120
+ let [previous] = allLocs;
6121
+ for (let i = 1; i < allLocs.length; i++) {
6122
+ if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
6123
+ previous = {
6124
+ min: Math.min(previous.min, allLocs[i].min),
6125
+ max: Math.max(previous.max, allLocs[i].max),
6126
+ };
6127
+ }
6128
+ else {
6129
+ locIntervals.push(previous);
6130
+ previous = allLocs[i];
6131
+ }
6132
+ }
6133
+ locIntervals.push(previous);
6134
+ return locIntervals;
6135
+ }
6122
6136
  const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ assembly, feature, refName, session, }) {
6123
6137
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
6124
6138
  const refData = currentAssembly?.getByRefName(refName);
6125
- const [showSequence, setShowSequence] = React.useState(false);
6126
- const [selectedOption, setSelectedOption] = React.useState('CDS');
6139
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
6140
+ const defaultSelectedOption = 'genomic';
6141
+ const defaultSequenceOptions = ['genomic', 'cDNA'];
6142
+ const [sequenceOptions, setSequenceOptions] = React.useState(defaultSequenceOptions);
6143
+ const [selectedOption, setSelectedOption] = React.useState(defaultSelectedOption);
6144
+ const [sequenceSegments, setSequenceSegments] = React.useState(() => {
6145
+ return refData
6146
+ ? getSequenceSegments(defaultSelectedOption, feature, (min, max) => refData.getSequence(min, max))
6147
+ : [];
6148
+ });
6149
+ const [locationIntervals, setLocationIntervals] = React.useState(() => {
6150
+ return getLocationIntervals(sequenceSegments);
6151
+ });
6127
6152
  const theme = material.useTheme();
6128
6153
  const seqRef = React.useRef(null);
6154
+ React.useEffect(() => {
6155
+ const { cdsLocations } = feature;
6156
+ const [firstLocation] = cdsLocations;
6157
+ if (firstLocation.length > 0) {
6158
+ setSequenceOptions([...defaultSequenceOptions, 'CDS', 'protein']);
6159
+ }
6160
+ else {
6161
+ setSequenceOptions(defaultSequenceOptions);
6162
+ }
6163
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6164
+ }, [feature]);
6129
6165
  if (!(currentAssembly && refData)) {
6130
6166
  return null;
6131
6167
  }
@@ -6133,15 +6169,21 @@ const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ asse
6133
6169
  if (!refSeq) {
6134
6170
  return null;
6135
6171
  }
6136
- if (feature.type !== 'mRNA') {
6172
+ if (!featureTypeOntology) {
6173
+ throw new Error('featureTypeOntology is undefined');
6174
+ }
6175
+ if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
6137
6176
  return null;
6138
6177
  }
6139
- const handleSeqButtonClick = () => {
6140
- setShowSequence(!showSequence);
6141
- };
6142
6178
  function handleChangeSeqOption(e) {
6143
6179
  const option = e.target.value;
6144
6180
  setSelectedOption(option);
6181
+ const seqSegments = refData
6182
+ ? getSequenceSegments(option, feature, (min, max) => refData.getSequence(min, max))
6183
+ : [];
6184
+ const locIntervals = getLocationIntervals(seqSegments);
6185
+ setSequenceSegments(seqSegments);
6186
+ setLocationIntervals(locIntervals);
6145
6187
  }
6146
6188
  // Function to copy text to clipboard
6147
6189
  const copyToClipboard = () => {
@@ -6157,62 +6199,419 @@ const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ asse
6157
6199
  });
6158
6200
  void navigator.clipboard.write([clipboardItem]);
6159
6201
  };
6160
- const sequenceSegments = showSequence
6161
- ? getSequenceSegments(selectedOption, feature, (min, max) => refData.getSequence(min, max))
6162
- : [];
6163
- const locationIntervals = [];
6164
- if (showSequence) {
6165
- const allLocs = sequenceSegments.flatMap((segment) => segment.locs);
6166
- let [previous] = allLocs;
6167
- for (let i = 1; i < allLocs.length; i++) {
6168
- if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
6169
- previous = {
6170
- min: Math.min(previous.min, allLocs[i].min),
6171
- max: Math.max(previous.max, allLocs[i].max),
6172
- };
6202
+ return (React__default["default"].createElement(React__default["default"].Fragment, null,
6203
+ React__default["default"].createElement(material.Select, { defaultValue: "genomic", value: selectedOption, onChange: handleChangeSeqOption, size: "small" }, sequenceOptions.map((option) => (React__default["default"].createElement(material.MenuItem, { key: option, value: option }, option)))),
6204
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: copyToClipboard, style: { marginLeft: 10 }, size: "medium" }, "Copy sequence"),
6205
+ React__default["default"].createElement(material.Paper, { style: {
6206
+ fontFamily: 'monospace',
6207
+ padding: theme.spacing(),
6208
+ overflowX: 'auto',
6209
+ }, ref: seqRef },
6210
+ ">",
6211
+ refSeq.name,
6212
+ ":",
6213
+ locationIntervals
6214
+ .map((interval) => feature.strand === 1
6215
+ ? `${interval.min + 1}-${interval.max}`
6216
+ : `${interval.max}-${interval.min + 1}`)
6217
+ .join(';'),
6218
+ "(",
6219
+ feature.strand === 1 ? '+' : '-',
6220
+ ")",
6221
+ React__default["default"].createElement("br", null),
6222
+ sequenceSegments.map((segment, index) => (React__default["default"].createElement("span", { key: `${segment.type}-${index}`, style: {
6223
+ background: getSegmentColor(segment.type),
6224
+ color: theme.palette.getContrastText(getSegmentColor(segment.type)),
6225
+ } }, segment.sequenceLines.map((sequenceLine, idx) => (React__default["default"].createElement(React__default["default"].Fragment, { key: `${sequenceLine.slice(0, 5)}-${idx}` },
6226
+ sequenceLine,
6227
+ idx === segment.sequenceLines.length - 1 &&
6228
+ sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (React__default["default"].createElement("br", null)))))))))));
6229
+ });
6230
+
6231
+ const HeaderTableCell = styled__default["default"](material.TableCell)(() => ({
6232
+ fontWeight: 'bold',
6233
+ }));
6234
+ const TranscriptWidgetSummary = mobxReact.observer(function TranscriptWidgetSummary(props) {
6235
+ const { feature } = props;
6236
+ const name = getFeatureName(feature);
6237
+ const id = getFeatureId$1(feature);
6238
+ return (React__default["default"].createElement(material.Table, { size: "small", sx: { fontSize: '0.75rem', '& .MuiTableCell-root': { padding: '4px' } } },
6239
+ React__default["default"].createElement(material.TableBody, null,
6240
+ name !== '' && (React__default["default"].createElement(material.TableRow, null,
6241
+ React__default["default"].createElement(HeaderTableCell, null, "Name"),
6242
+ React__default["default"].createElement(material.TableCell, null, getFeatureName(feature)))),
6243
+ id !== '' && (React__default["default"].createElement(material.TableRow, null,
6244
+ React__default["default"].createElement(HeaderTableCell, null, "ID"),
6245
+ React__default["default"].createElement(material.TableCell, null, getFeatureId$1(feature)))),
6246
+ React__default["default"].createElement(material.TableRow, null,
6247
+ React__default["default"].createElement(HeaderTableCell, null, "Location"),
6248
+ React__default["default"].createElement(material.TableCell, null,
6249
+ props.refName,
6250
+ ":",
6251
+ feature.min,
6252
+ "..",
6253
+ feature.max)),
6254
+ React__default["default"].createElement(material.TableRow, null,
6255
+ React__default["default"].createElement(HeaderTableCell, null, "Strand"),
6256
+ React__default["default"].createElement(material.TableCell, null, getStrand(feature.strand))))));
6257
+ });
6258
+
6259
+ /* eslint-disable unicorn/no-nested-ternary */
6260
+ const StyledTextField = styled__default["default"](NumberTextField)(() => ({
6261
+ '&.MuiFormControl-root': {
6262
+ marginTop: 0,
6263
+ marginBottom: 0,
6264
+ width: '100%',
6265
+ },
6266
+ '& .MuiInputBase-input': {
6267
+ fontSize: 12,
6268
+ height: 20,
6269
+ padding: 1,
6270
+ paddingLeft: 10,
6271
+ },
6272
+ }));
6273
+ const SequenceContainer = styled__default["default"]('div')({
6274
+ display: 'flex',
6275
+ justifyContent: 'center',
6276
+ alignItems: 'center',
6277
+ textAlign: 'left',
6278
+ width: '100%',
6279
+ overflowWrap: 'break-word',
6280
+ wordWrap: 'break-word',
6281
+ wordBreak: 'break-all',
6282
+ '& span': {
6283
+ fontSize: 12,
6284
+ },
6285
+ });
6286
+ const Strand = (props) => {
6287
+ const { strand } = props;
6288
+ return (React__default["default"].createElement("div", null, strand === 1 ? (React__default["default"].createElement(AddIcon__default["default"], null)) : strand === -1 ? (React__default["default"].createElement(RemoveIcon__default["default"], null)) : (React__default["default"].createElement(material.Typography, { component: 'span' }, "N/A"))));
6289
+ };
6290
+ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidgetEditLocation({ assembly, feature, refName, session, }) {
6291
+ const { notify } = session;
6292
+ const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
6293
+ const refData = currentAssembly?.getByRefName(refName);
6294
+ const { changeManager } = session.apolloDataStore;
6295
+ const seqRef = React.useRef(null);
6296
+ // Separate function to handle CDS location change
6297
+ // because start of CDS and exon might be same
6298
+ function handleCDSLocationChange(oldLocation, newLocation, feature, isMin) {
6299
+ if (!feature.children) {
6300
+ throw new Error('Transcript should have child features');
6301
+ }
6302
+ for (const [, child] of feature.children) {
6303
+ if (child.type !== 'CDS') {
6304
+ continue;
6173
6305
  }
6174
- else {
6175
- locationIntervals.push(previous);
6176
- previous = allLocs[i];
6306
+ if (isMin && oldLocation === child.min) {
6307
+ const change = new shared.LocationStartChange({
6308
+ typeName: 'LocationStartChange',
6309
+ changedIds: [child._id],
6310
+ featureId: feature._id,
6311
+ oldStart: child.min,
6312
+ newStart: newLocation,
6313
+ assembly,
6314
+ });
6315
+ changeManager.submit(change).catch(() => {
6316
+ notify('Error updating feature start position', 'error');
6317
+ });
6318
+ return;
6319
+ }
6320
+ if (!isMin && oldLocation === child.max) {
6321
+ const change = new shared.LocationEndChange({
6322
+ typeName: 'LocationEndChange',
6323
+ changedIds: [child._id],
6324
+ featureId: feature._id,
6325
+ oldEnd: child.max,
6326
+ newEnd: newLocation,
6327
+ assembly,
6328
+ });
6329
+ changeManager.submit(change).catch(() => {
6330
+ notify('Error updating feature start position', 'error');
6331
+ });
6332
+ return;
6177
6333
  }
6178
6334
  }
6179
- locationIntervals.push(previous);
6180
6335
  }
6181
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
6182
- React__default["default"].createElement(material.Typography, { variant: "h5" }, "Sequence"),
6183
- React__default["default"].createElement("div", null,
6184
- React__default["default"].createElement(material.Button, { variant: "contained", onClick: handleSeqButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence')),
6185
- showSequence && (React__default["default"].createElement(React__default["default"].Fragment, null,
6186
- React__default["default"].createElement(material.Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
6187
- React__default["default"].createElement(material.MenuItem, { value: "CDS" }, "CDS"),
6188
- React__default["default"].createElement(material.MenuItem, { value: "cDNA" }, "cDNA"),
6189
- React__default["default"].createElement(material.MenuItem, { value: "genomic" }, "Genomic"),
6190
- React__default["default"].createElement(material.MenuItem, { value: "protein" }, "Protein")),
6191
- React__default["default"].createElement(material.Paper, { style: {
6192
- fontFamily: 'monospace',
6193
- padding: theme.spacing(),
6194
- overflowX: 'auto',
6195
- }, ref: seqRef },
6196
- ">",
6197
- refSeq.name,
6198
- ":",
6199
- locationIntervals
6200
- .map((interval) => feature.strand === 1
6201
- ? `${interval.min + 1}-${interval.max}`
6202
- : `${interval.max}-${interval.min + 1}`)
6203
- .join(';'),
6204
- "(",
6205
- feature.strand === 1 ? '+' : '-',
6206
- ")",
6207
- React__default["default"].createElement("br", null),
6208
- sequenceSegments.map((segment, index) => (React__default["default"].createElement("span", { key: `${segment.type}-${index}`, style: {
6209
- background: getSegmentColor(segment.type),
6210
- color: theme.palette.getContrastText(getSegmentColor(segment.type)),
6211
- } }, segment.sequenceLines.map((sequenceLine, idx) => (React__default["default"].createElement(React__default["default"].Fragment, { key: `${sequenceLine.slice(0, 5)}-${idx}` },
6212
- sequenceLine,
6213
- idx === segment.sequenceLines.length - 1 &&
6214
- sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (React__default["default"].createElement("br", null))))))))),
6215
- React__default["default"].createElement(material.Button, { variant: "contained", onClick: copyToClipboard }, "Copy sequence")))));
6336
+ function handleExonLocationChange(oldLocation, newLocation, feature, isMin) {
6337
+ if (!feature.children) {
6338
+ throw new Error('Transcript should have child features');
6339
+ }
6340
+ for (const [, child] of feature.children) {
6341
+ if (child.type !== 'exon') {
6342
+ continue;
6343
+ }
6344
+ if (isMin && oldLocation === child.min) {
6345
+ const change = new shared.LocationStartChange({
6346
+ typeName: 'LocationStartChange',
6347
+ changedIds: [child._id],
6348
+ featureId: feature._id,
6349
+ oldStart: child.min,
6350
+ newStart: newLocation,
6351
+ assembly,
6352
+ });
6353
+ changeManager.submit(change).catch(() => {
6354
+ notify('Error updating feature start position', 'error');
6355
+ });
6356
+ return;
6357
+ }
6358
+ if (!isMin && oldLocation === child.max) {
6359
+ const change = new shared.LocationEndChange({
6360
+ typeName: 'LocationEndChange',
6361
+ changedIds: [child._id],
6362
+ featureId: feature._id,
6363
+ oldEnd: child.max,
6364
+ newEnd: newLocation,
6365
+ assembly,
6366
+ });
6367
+ changeManager.submit(change).catch(() => {
6368
+ notify('Error updating feature start position', 'error');
6369
+ });
6370
+ return;
6371
+ }
6372
+ }
6373
+ }
6374
+ if (!refData) {
6375
+ return null;
6376
+ }
6377
+ const { cdsLocations, transcriptExonParts, strand } = feature;
6378
+ const [firstCDSLocation] = cdsLocations;
6379
+ const exonParts = transcriptExonParts
6380
+ .filter((part) => part.type === 'exon')
6381
+ .sort(({ min: a }, { min: b }) => a - b);
6382
+ const exonMin = exonParts[0]?.min;
6383
+ const exonMax = exonParts[exonParts.length - 1]?.max;
6384
+ let cdsMin = exonMin;
6385
+ let cdsMax = exonMax;
6386
+ const cdsPresent = firstCDSLocation.length > 0;
6387
+ if (cdsPresent) {
6388
+ cdsMin = firstCDSLocation[0].min;
6389
+ cdsMax = firstCDSLocation[firstCDSLocation.length - 1].max;
6390
+ }
6391
+ const getFivePrimeSpliceSite = (loc, prevLocIdx) => {
6392
+ let spliceSite = '';
6393
+ if (prevLocIdx > 0) {
6394
+ const prevLoc = transcriptExonParts[prevLocIdx - 1];
6395
+ if (strand === 1) {
6396
+ if (prevLoc.type === 'intron') {
6397
+ spliceSite = refData.getSequence(loc.min - 2, loc.min);
6398
+ }
6399
+ }
6400
+ else {
6401
+ if (prevLoc.type === 'intron') {
6402
+ spliceSite = util.revcom(refData.getSequence(loc.max, loc.max + 2));
6403
+ }
6404
+ }
6405
+ }
6406
+ return [
6407
+ {
6408
+ spliceSite,
6409
+ color: spliceSite === 'AG' ? 'green' : 'red',
6410
+ },
6411
+ ];
6412
+ };
6413
+ const getThreePrimeSpliceSite = (loc, nextLocIdx) => {
6414
+ let spliceSite = '';
6415
+ if (nextLocIdx < transcriptExonParts.length - 1) {
6416
+ const nextLoc = transcriptExonParts[nextLocIdx + 1];
6417
+ if (strand === 1) {
6418
+ if (nextLoc.type === 'intron') {
6419
+ spliceSite = refData.getSequence(loc.max, loc.max + 2);
6420
+ }
6421
+ }
6422
+ else {
6423
+ if (nextLoc.type === 'intron') {
6424
+ spliceSite = util.revcom(refData.getSequence(loc.min - 2, loc.min));
6425
+ }
6426
+ }
6427
+ }
6428
+ return [
6429
+ {
6430
+ spliceSite,
6431
+ color: spliceSite === 'GT' ? 'green' : 'red',
6432
+ },
6433
+ ];
6434
+ };
6435
+ const getTranslationSequence = () => {
6436
+ let wholeSequence = '';
6437
+ const [firstLocation] = cdsLocations;
6438
+ for (const loc of firstLocation) {
6439
+ let sequence = refData.getSequence(loc.min, loc.max);
6440
+ if (strand === -1) {
6441
+ sequence = util.revcom(sequence);
6442
+ }
6443
+ wholeSequence += sequence;
6444
+ }
6445
+ const elements = [];
6446
+ for (let codonGenomicPos = 0; codonGenomicPos < wholeSequence.length; codonGenomicPos += 3) {
6447
+ const codonSeq = wholeSequence
6448
+ .slice(codonGenomicPos, codonGenomicPos + 3)
6449
+ .toUpperCase();
6450
+ const protein = util.defaultCodonTable[codonSeq] || '&';
6451
+ // highlight start codon and stop codons
6452
+ if (codonSeq === 'ATG') {
6453
+ elements.push(React__default["default"].createElement(material.Typography, { component: 'span', style: {
6454
+ backgroundColor: 'yellow',
6455
+ cursor: 'pointer',
6456
+ border: '1px solid black',
6457
+ }, key: codonGenomicPos, onClick: () => {
6458
+ // NOTE: codonGenomicPos is important here for calculating the genomic location
6459
+ // of the start codon. We are using the codonGenomicPos as the key in the typography
6460
+ // elements to maintain the genomic postion of the codon start
6461
+ const startCodonGenomicLocation = getStartCodonGenomicLocation(codonGenomicPos);
6462
+ if (startCodonGenomicLocation !== cdsMin) {
6463
+ handleCDSLocationChange(cdsMin, startCodonGenomicLocation, feature, true);
6464
+ }
6465
+ } }, protein));
6466
+ }
6467
+ else if (['TAA', 'TAG', 'TGA'].includes(codonSeq)) {
6468
+ elements.push(React__default["default"].createElement(material.Typography, { style: { backgroundColor: 'red', color: 'white' }, component: 'span',
6469
+ // Pass the codonGenomicPos as the key to maintain the genomic position of the codon
6470
+ key: codonGenomicPos }, protein));
6471
+ }
6472
+ else {
6473
+ elements.push(
6474
+ // Pass the codonGenomicPos as the key to maintain the genomic position of the codon
6475
+ React__default["default"].createElement(material.Typography, { component: 'span', key: codonGenomicPos }, protein));
6476
+ }
6477
+ }
6478
+ return elements;
6479
+ };
6480
+ // Codon position is the index of the start codon in the CDS genomic sequence
6481
+ // Calculate the genomic location of the start codon based on the codon position in the CDS
6482
+ const getStartCodonGenomicLocation = (codonGenomicPosition) => {
6483
+ const [firstLocation] = cdsLocations;
6484
+ let cdsLen = 0;
6485
+ for (const loc of firstLocation) {
6486
+ const locLength = loc.max - loc.min;
6487
+ // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
6488
+ // and codonGenomicPosition is 25
6489
+ // (((10 - 0) + (30 - 20)) + 10) > 25
6490
+ // 40 + (25-20) = 45 is the genomic location of the start codon
6491
+ if (cdsLen + locLength > codonGenomicPosition) {
6492
+ return loc.min + (codonGenomicPosition - cdsLen);
6493
+ }
6494
+ cdsLen += locLength;
6495
+ }
6496
+ return cdsMin;
6497
+ };
6498
+ const getStopCodonGenomicLocation = (codonGenomicPosition) => {
6499
+ const [firstLocation] = cdsLocations;
6500
+ let cdsLen = 0;
6501
+ for (const loc of firstLocation) {
6502
+ const locLength = loc.max - loc.min;
6503
+ // Check if the codonPosition is within the current location
6504
+ if (cdsLen + locLength > codonGenomicPosition) {
6505
+ return loc.min + (codonGenomicPosition - cdsLen);
6506
+ }
6507
+ cdsLen += locLength;
6508
+ }
6509
+ return cdsMax;
6510
+ };
6511
+ const trimTranslationSequence = () => {
6512
+ const sequenceElements = getTranslationSequence();
6513
+ const translationSequence = sequenceElements
6514
+ .map((el) => el.props.children)
6515
+ .join('');
6516
+ if (translationSequence.startsWith('M') &&
6517
+ translationSequence.endsWith('*')) {
6518
+ return;
6519
+ }
6520
+ // NOTE: We are maintaining the genomic location of the codon start as the "key"
6521
+ // in typography elements. See getTranslationSequence function
6522
+ const translSeqCodonStartGenomicPosArr = [];
6523
+ for (const el of sequenceElements) {
6524
+ translSeqCodonStartGenomicPosArr.push({
6525
+ codonGenomicPos: el.key,
6526
+ sequenceLetter: el.props.children,
6527
+ });
6528
+ }
6529
+ if (translSeqCodonStartGenomicPosArr.length === 0) {
6530
+ return;
6531
+ }
6532
+ // Trim any sequence before first start codon and after last stop codon
6533
+ const startCodonIndex = translationSequence.indexOf('M');
6534
+ const stopCodonIndex = translationSequence.lastIndexOf('*') + 1;
6535
+ const startCodonPos = translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos;
6536
+ const stopCodonPos = translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos;
6537
+ if (!startCodonPos || !stopCodonPos) {
6538
+ return;
6539
+ }
6540
+ const startCodonGenomicLoc = getStartCodonGenomicLocation(startCodonPos);
6541
+ const stopCodonGenomicLoc = getStopCodonGenomicLocation(stopCodonPos);
6542
+ if (startCodonGenomicLoc !== cdsMin) {
6543
+ handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true);
6544
+ }
6545
+ if (stopCodonGenomicLoc !== cdsMax) {
6546
+ // TODO: getting error when trying to change the CDS start and end location at the same time
6547
+ // Need to fix this
6548
+ setTimeout(() => {
6549
+ handleCDSLocationChange(cdsMax, stopCodonGenomicLoc, feature, false);
6550
+ }, 1000);
6551
+ }
6552
+ };
6553
+ const copyToClipboard = () => {
6554
+ const seqDiv = seqRef.current;
6555
+ if (!seqDiv) {
6556
+ return;
6557
+ }
6558
+ const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' });
6559
+ const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
6560
+ const clipboardItem = new ClipboardItem({
6561
+ [textBlob.type]: textBlob,
6562
+ [htmlBlob.type]: htmlBlob,
6563
+ });
6564
+ void navigator.clipboard.write([clipboardItem]);
6565
+ };
6566
+ return (React__default["default"].createElement("div", null,
6567
+ cdsPresent && (React__default["default"].createElement("div", null,
6568
+ React__default["default"].createElement(material.Accordion, { defaultExpanded: true },
6569
+ React__default["default"].createElement(StyledAccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
6570
+ React__default["default"].createElement(material.Typography, { component: "span", fontWeight: 'bold' }, "Translation")),
6571
+ React__default["default"].createElement(material.AccordionDetails, null,
6572
+ React__default["default"].createElement(SequenceContainer, null,
6573
+ React__default["default"].createElement(material.Typography, { component: 'span', ref: seqRef }, getTranslationSequence())),
6574
+ React__default["default"].createElement("div", { style: {
6575
+ marginTop: 10,
6576
+ display: 'flex',
6577
+ flexDirection: 'row',
6578
+ alignItems: 'center',
6579
+ gap: 10,
6580
+ } },
6581
+ React__default["default"].createElement(material.Tooltip, { title: "Copy" },
6582
+ React__default["default"].createElement(ContentCopyIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick: copyToClipboard })),
6583
+ React__default["default"].createElement(material.Tooltip, { title: "Trim" },
6584
+ React__default["default"].createElement(ContentCutIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick: trimTranslationSequence }))))),
6585
+ React__default["default"].createElement(material.Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center', marginTop: 10 } },
6586
+ React__default["default"].createElement(material.Grid2, { size: 1 }),
6587
+ React__default["default"].createElement(material.Grid2, { size: 4 },
6588
+ React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin, onChangeCommitted: (newLocation) => {
6589
+ handleCDSLocationChange(cdsMin, newLocation, feature, true);
6590
+ } })),
6591
+ React__default["default"].createElement(material.Grid2, { size: 2 },
6592
+ React__default["default"].createElement(material.Typography, { component: 'span' }, "CDS")),
6593
+ React__default["default"].createElement(material.Grid2, { size: 4 },
6594
+ React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
6595
+ handleCDSLocationChange(cdsMax, newLocation, feature, false);
6596
+ } })),
6597
+ React__default["default"].createElement(material.Grid2, { size: 1 })))),
6598
+ React__default["default"].createElement("div", { style: { marginTop: 5 } }, transcriptExonParts.map((loc, index) => {
6599
+ return (React__default["default"].createElement("div", { key: index }, loc.type === 'exon' && (React__default["default"].createElement(material.Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center' } },
6600
+ React__default["default"].createElement(material.Grid2, { size: 1 }, index !== 0 &&
6601
+ getFivePrimeSpliceSite(loc, index).map((site, idx) => (React__default["default"].createElement(material.Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite)))),
6602
+ React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
6603
+ React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min, onChangeCommitted: (newLocation) => {
6604
+ handleExonLocationChange(loc.min, newLocation, feature, true);
6605
+ } })),
6606
+ React__default["default"].createElement(material.Grid2, { size: 2 },
6607
+ React__default["default"].createElement(Strand, { strand: feature.strand })),
6608
+ React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
6609
+ React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
6610
+ handleExonLocationChange(loc.max, newLocation, feature, false);
6611
+ } })),
6612
+ React__default["default"].createElement(material.Grid2, { size: 1 }, index !== transcriptExonParts.length - 1 &&
6613
+ getThreePrimeSpliceSite(loc, index).map((site, idx) => (React__default["default"].createElement(material.Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite))))))));
6614
+ }))));
6216
6615
  });
6217
6616
 
6218
6617
  const useStyles$7 = mui.makeStyles()((theme) => ({
@@ -6220,10 +6619,24 @@ const useStyles$7 = mui.makeStyles()((theme) => ({
6220
6619
  padding: theme.spacing(2),
6221
6620
  },
6222
6621
  }));
6622
+ const StyledAccordionSummary = styled__default["default"](material.AccordionSummary)(() => ({
6623
+ minHeight: 30,
6624
+ maxHeight: 30,
6625
+ '&.Mui-expanded': {
6626
+ minHeight: 30,
6627
+ maxHeight: 30,
6628
+ },
6629
+ }));
6223
6630
  const ApolloTranscriptDetailsWidget = mobxReact.observer(function ApolloTranscriptDetails(props) {
6224
6631
  const { classes } = useStyles$7();
6632
+ const DEFAULT_PANELS = ['summary', 'location', 'attrs'];
6633
+ const [panelState, setPanelState] = React.useState(DEFAULT_PANELS);
6225
6634
  const { model } = props;
6226
6635
  const { assembly, feature, refName } = model;
6636
+ React.useEffect(() => {
6637
+ setPanelState(DEFAULT_PANELS);
6638
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6639
+ }, [feature]);
6227
6640
  const session = util.getSession(model);
6228
6641
  const apolloSession = util.getSession(model);
6229
6642
  const currentAssembly = apolloSession.apolloDataStore.assemblies.get(assembly);
@@ -6245,12 +6658,47 @@ const ApolloTranscriptDetailsWidget = mobxReact.observer(function ApolloTranscri
6245
6658
  { assemblyName: assembly, refName, start: min, end: max },
6246
6659
  ]);
6247
6660
  }
6661
+ function handlePanelChange(expanded, panel) {
6662
+ if (expanded) {
6663
+ setPanelState([...panelState, panel]);
6664
+ }
6665
+ else {
6666
+ setPanelState(panelState.filter((p) => p !== panel));
6667
+ }
6668
+ }
6248
6669
  return (React__default["default"].createElement("div", { className: classes.root },
6249
- React__default["default"].createElement(TranscriptBasicInformation, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName }),
6250
- React__default["default"].createElement("hr", null),
6251
- React__default["default"].createElement(Attributes, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', editable: editable }),
6252
- React__default["default"].createElement("hr", null),
6253
- React__default["default"].createElement(TranscriptSequence, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName })));
6670
+ React__default["default"].createElement(material.Accordion, { expanded: panelState.includes('summary'), onChange: (e, expanded) => {
6671
+ handlePanelChange(expanded, 'summary');
6672
+ } },
6673
+ React__default["default"].createElement(StyledAccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
6674
+ React__default["default"].createElement(material.Typography, { component: "span", fontWeight: 'bold' }, "Summary")),
6675
+ React__default["default"].createElement(material.AccordionDetails, null,
6676
+ React__default["default"].createElement(TranscriptWidgetSummary, { feature: feature, refName: refName }))),
6677
+ React__default["default"].createElement(material.Accordion, { style: { marginTop: 5 }, expanded: panelState.includes('location'), onChange: (e, expanded) => {
6678
+ handlePanelChange(expanded, 'location');
6679
+ } },
6680
+ React__default["default"].createElement(StyledAccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel2-content", id: "panel2-header" },
6681
+ React__default["default"].createElement(material.Typography, { component: "span", fontWeight: 'bold' }, "Location")),
6682
+ React__default["default"].createElement(material.AccordionDetails, null,
6683
+ React__default["default"].createElement(TranscriptWidgetEditLocation, { feature: feature, refName: refName, session: apolloSession, assembly: currentAssembly._id || '' }))),
6684
+ React__default["default"].createElement(material.Accordion, { style: { marginTop: 5 }, expanded: panelState.includes('attrs'), onChange: (e, expanded) => {
6685
+ handlePanelChange(expanded, 'attrs');
6686
+ } },
6687
+ React__default["default"].createElement(StyledAccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel3-content", id: "panel3-header" },
6688
+ React__default["default"].createElement("div", { style: { display: 'flex', alignItems: 'center' } },
6689
+ React__default["default"].createElement(material.Typography, { component: "span", fontWeight: 'bold' },
6690
+ "Attributes",
6691
+ ' '),
6692
+ React__default["default"].createElement(material.Tooltip, { title: "Separate multiple values for the attribute with commas" },
6693
+ React__default["default"].createElement(InfoIcon__default["default"], { style: { color: 'white', fontSize: 15, marginLeft: 10 } })))),
6694
+ React__default["default"].createElement(material.AccordionDetails, null,
6695
+ React__default["default"].createElement(Attributes, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', editable: editable }))),
6696
+ React__default["default"].createElement(material.Accordion, { style: { marginTop: 5 }, expanded: panelState.includes('sequence'), onChange: (e, expanded) => {
6697
+ handlePanelChange(expanded, 'sequence');
6698
+ } },
6699
+ React__default["default"].createElement(StyledAccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel4-content", id: "panel4-header" },
6700
+ React__default["default"].createElement(material.Typography, { component: "span", fontWeight: 'bold' }, "Sequence")),
6701
+ React__default["default"].createElement(material.AccordionDetails, null, panelState.includes('sequence') && (React__default["default"].createElement(TranscriptSequence, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName }))))));
6254
6702
  });
6255
6703
 
6256
6704
  const configSchema$1 = configuration.ConfigurationSchema('LinearApolloDisplay', {}, { explicitIdentifier: 'displayId', explicitlyTyped: true });
@@ -6407,29 +6855,13 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
6407
6855
  },
6408
6856
  ]);
6409
6857
  },
6410
- }, {
6411
- label: 'Edit attributes',
6412
- disabled: readOnly,
6413
- onClick: () => {
6414
- session.queueDialog((doneCallback) => [
6415
- ModifyFeatureAttribute,
6416
- {
6417
- session,
6418
- handleClose: () => {
6419
- doneCallback();
6420
- },
6421
- changeManager,
6422
- sourceFeature: feature,
6423
- sourceAssemblyId: currentAssemblyId,
6424
- },
6425
- ]);
6426
- },
6427
6858
  });
6428
6859
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
6429
6860
  if (!featureTypeOntology) {
6430
6861
  throw new Error('featureTypeOntology is undefined');
6431
6862
  }
6432
- if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
6863
+ if ((featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
6864
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
6433
6865
  util.isSessionModelWithWidgets(session)) {
6434
6866
  menuItems.push({
6435
6867
  label: 'Edit transcript details',
@@ -6471,6 +6903,12 @@ const NumberCell = mobxReact.observer(function NumberCell({ initialValue, notify
6471
6903
  const [blur, setBlur] = React.useState(false);
6472
6904
  const [inputNode, setInputNode] = React.useState(null);
6473
6905
  const { classes } = useStyles$5();
6906
+ React.useEffect(() => {
6907
+ if (initialValue !== value) {
6908
+ setValue(initialValue);
6909
+ }
6910
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6911
+ }, [initialValue]);
6474
6912
  React.useEffect(() => {
6475
6913
  if (blur) {
6476
6914
  inputNode?.blur();
@@ -6502,7 +6940,7 @@ const NumberCell = mobxReact.observer(function NumberCell({ initialValue, notify
6502
6940
  } })));
6503
6941
  });
6504
6942
 
6505
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
6943
+ /* eslint-disable unicorn/no-nested-ternary */
6506
6944
  const useStyles$4 = mui.makeStyles()((theme) => ({
6507
6945
  typeContent: {
6508
6946
  display: 'inline-block',
@@ -6746,12 +7184,14 @@ const ToolBar = mobxReact.observer(function ToolBar({ model: displayState, }) {
6746
7184
  React__default["default"].createElement(UnfoldLessIcon__default["default"], null))),
6747
7185
  React__default["default"].createElement(material.TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
6748
7186
  model.setFilterText(event.target.value);
6749
- }, InputProps: {
6750
- endAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "end" },
6751
- React__default["default"].createElement(material.IconButton, { onClick: () => {
6752
- model.clearFilterText();
6753
- } },
6754
- React__default["default"].createElement(ClearIcon__default["default"], null)))),
7187
+ }, slotProps: {
7188
+ input: {
7189
+ endAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "end" },
7190
+ React__default["default"].createElement(material.IconButton, { onClick: () => {
7191
+ model.clearFilterText();
7192
+ } },
7193
+ React__default["default"].createElement(ClearIcon__default["default"], null)))),
7194
+ },
6755
7195
  } })));
6756
7196
  });
6757
7197
 
@@ -7305,23 +7745,6 @@ function getContextMenuItems$2(display) {
7305
7745
  },
7306
7746
  ]);
7307
7747
  },
7308
- }, {
7309
- label: 'Modify feature attribute',
7310
- disabled: readOnly,
7311
- onClick: () => {
7312
- session.queueDialog((doneCallback) => [
7313
- ModifyFeatureAttribute,
7314
- {
7315
- session,
7316
- handleClose: () => {
7317
- doneCallback();
7318
- },
7319
- changeManager,
7320
- sourceFeature,
7321
- sourceAssemblyId: currentAssemblyId,
7322
- },
7323
- ]);
7324
- },
7325
7748
  }, {
7326
7749
  label: 'Edit feature details',
7327
7750
  onClick: () => {
@@ -7337,7 +7760,8 @@ function getContextMenuItems$2(display) {
7337
7760
  if (!featureTypeOntology) {
7338
7761
  throw new Error('featureTypeOntology is undefined');
7339
7762
  }
7340
- if (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
7763
+ if ((featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
7764
+ featureTypeOntology.isTypeOf(sourceFeature.type, 'pseudogenic_transcript')) &&
7341
7765
  util.isSessionModelWithWidgets(session)) {
7342
7766
  menuItems.push({
7343
7767
  label: 'Edit transcript details',
@@ -7517,7 +7941,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7517
7941
  // Draw lines on different rows for each transcript
7518
7942
  let currentRow = 0;
7519
7943
  for (const [, transcript] of children) {
7520
- const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript');
7944
+ const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript') ||
7945
+ featureTypeOntology.isTypeOf(transcript.type, 'pseudogenic_transcript');
7521
7946
  if (!isTranscript) {
7522
7947
  currentRow += 1;
7523
7948
  continue;
@@ -7544,7 +7969,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7544
7969
  // Draw exon and CDS for each transcript
7545
7970
  currentRow = 0;
7546
7971
  for (const [, child] of children) {
7547
- if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
7972
+ if (!(featureTypeOntology.isTypeOf(child.type, 'transcript') ||
7973
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript'))) {
7548
7974
  boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex);
7549
7975
  currentRow += 1;
7550
7976
  continue;
@@ -7734,7 +8160,8 @@ function getFeatureFromLayout$1(feature, bp, row, featureTypeOntology) {
7734
8160
  }
7735
8161
  if (featureTypeOntology.isTypeOf(featureObj.type, 'CDS') &&
7736
8162
  featureObj.parent &&
7737
- featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript')) {
8163
+ (featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript') ||
8164
+ featureTypeOntology.isTypeOf(featureObj.parent.type, 'pseudogenic_transcript'))) {
7738
8165
  const { cdsLocations } = featureObj.parent;
7739
8166
  for (const cdsLoc of cdsLocations) {
7740
8167
  for (const loc of cdsLoc) {
@@ -7756,7 +8183,7 @@ function getCDSCount(feature, featureTypeOntology) {
7756
8183
  if (!children) {
7757
8184
  return 0;
7758
8185
  }
7759
- const isMrna = featureTypeOntology.isTypeOf(type, 'mRNA');
8186
+ const isMrna = featureTypeOntology.isTypeOf(type, 'transcript');
7760
8187
  let cdsCount = 0;
7761
8188
  if (isMrna) {
7762
8189
  for (const [, child] of children) {
@@ -7772,7 +8199,8 @@ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
7772
8199
  if (!children) {
7773
8200
  return 1;
7774
8201
  }
7775
- const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript');
8202
+ const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript') ||
8203
+ featureTypeOntology.isTypeOf(type, 'pseudogenic_transcript');
7776
8204
  let rowCount = 0;
7777
8205
  if (isTranscript) {
7778
8206
  for (const [, child] of children) {
@@ -7795,7 +8223,8 @@ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
7795
8223
  * If the row does not contain an transcript, the order is subfeature -\> gene
7796
8224
  */
7797
8225
  function featuresForRow$1(feature, featureTypeOntology) {
7798
- const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
8226
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') ||
8227
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
7799
8228
  if (!isGene) {
7800
8229
  throw new Error('Top level feature for GeneGlyph must have type "gene"');
7801
8230
  }
@@ -7805,7 +8234,8 @@ function featuresForRow$1(feature, featureTypeOntology) {
7805
8234
  }
7806
8235
  const features = [];
7807
8236
  for (const [, child] of children) {
7808
- if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
8237
+ if (!(featureTypeOntology.isTypeOf(child.type, 'transcript') ||
8238
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript'))) {
7809
8239
  features.push([child, feature]);
7810
8240
  continue;
7811
8241
  }
@@ -7880,8 +8310,10 @@ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7880
8310
  if (!featureTypeOntology) {
7881
8311
  throw new Error('featureTypeOntology is undefined');
7882
8312
  }
7883
- const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
7884
- const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript');
8313
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') ||
8314
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
8315
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
8316
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript');
7885
8317
  const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS');
7886
8318
  if (isGene || isTranscript) {
7887
8319
  return;
@@ -7918,7 +8350,7 @@ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7918
8350
  }
7919
8351
  }
7920
8352
  const overlappingExon = exonChildren.find((child) => {
7921
- const [start, end] = util.intersection2(bp, bp + 1, child.min, child.max);
8353
+ const [start, end] = util.intersection2(bp - 1, bp, child.min, child.max);
7922
8354
  return start !== undefined && end !== undefined;
7923
8355
  });
7924
8356
  if (!overlappingExon) {
@@ -8127,12 +8559,14 @@ function layoutsModelFactory(pluginManager, configSchema) {
8127
8559
  if (!children?.size) {
8128
8560
  return false;
8129
8561
  }
8130
- const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
8562
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') ||
8563
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
8131
8564
  if (!isGene) {
8132
8565
  return false;
8133
8566
  }
8134
8567
  for (const [, child] of children) {
8135
- if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
8568
+ if (featureTypeOntology.isTypeOf(child.type, 'transcript') ||
8569
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript')) {
8136
8570
  const { children: grandChildren } = child;
8137
8571
  if (!grandChildren?.size) {
8138
8572
  return false;
@@ -8402,6 +8836,8 @@ function codonColorCode(letter) {
8402
8836
  return colorMap[letter.toUpperCase()];
8403
8837
  }
8404
8838
  function reverseCodonSeq(seq) {
8839
+ // disable because sequence is all ascii
8840
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
8405
8841
  return [...seq]
8406
8842
  .map((c) => util.revcom(c))
8407
8843
  .reverse()
@@ -8472,6 +8908,8 @@ function sequenceRenderingModelFactory(pluginManager, configSchema) {
8472
8908
  if (!seq) {
8473
8909
  return;
8474
8910
  }
8911
+ // disable because sequence is all ascii
8912
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
8475
8913
  for (const [i, letter] of [...seq].entries()) {
8476
8914
  const trnslXOffset = (self.lgv.bpToPx({
8477
8915
  refName: region.refName,
@@ -8938,7 +9376,7 @@ const useStyles$1 = mui.makeStyles()((theme) => ({
8938
9376
  const LinearApolloDisplay = mobxReact.observer(function LinearApolloDisplay(props) {
8939
9377
  const theme = material.useTheme();
8940
9378
  const { model } = props;
8941
- const { loading, apolloRowHeight, contextMenuItems: getContextMenuItems, cursor, featuresHeight, isShown, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, regionCannotBeRendered, session, setCanvas, setCollaboratorCanvas, setOverlayCanvas, setSeqTrackCanvas, setSeqTrackOverlayCanvas, setTheme, } = model;
9379
+ const { loading, apolloDragging, apolloRowHeight, contextMenuItems: getContextMenuItems, cursor, featuresHeight, isShown, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, regionCannotBeRendered, session, setCanvas, setCollaboratorCanvas, setOverlayCanvas, setSeqTrackCanvas, setSeqTrackOverlayCanvas, setTheme, } = model;
8942
9380
  const { classes } = useStyles$1();
8943
9381
  const lgv = util.getContainingView(model);
8944
9382
  React.useEffect(() => {
@@ -9016,15 +9454,21 @@ const LinearApolloDisplay = mobxReact.observer(function LinearApolloDisplay(prop
9016
9454
  if (!feature) {
9017
9455
  return null;
9018
9456
  }
9019
- const { topLevelFeature } = feature;
9020
- const row = parent
9021
- ? model.getFeatureLayoutPosition(topLevelFeature)
9022
- ?.layoutRow ?? 0
9023
- : 0;
9457
+ let row = 0;
9458
+ const featureLayout = model.getFeatureLayoutPosition(feature);
9459
+ if (featureLayout) {
9460
+ row = featureLayout.layoutRow + featureLayout.featureRow;
9461
+ }
9024
9462
  const top = row * apolloRowHeight;
9025
9463
  const height = apolloRowHeight;
9026
9464
  return (React__default["default"].createElement(material.Tooltip, { key: checkResult._id, title: checkResult.message },
9027
- React__default["default"].createElement(material.Avatar, { className: classes.avatar, style: { top, left, height, width: height } },
9465
+ React__default["default"].createElement(material.Avatar, { className: classes.avatar, style: {
9466
+ top,
9467
+ left,
9468
+ height,
9469
+ width: height,
9470
+ pointerEvents: apolloDragging ? 'none' : 'auto',
9471
+ } },
9028
9472
  React__default["default"].createElement(ErrorIcon__default["default"], null))));
9029
9473
  });
9030
9474
  }),
@@ -9143,22 +9587,22 @@ const DisplayComponent = mobxReact.observer(function DisplayComponent({ model, .
9143
9587
  const { featureTypeOntology } = ontologyManager;
9144
9588
  const ontologyStore = featureTypeOntology?.dataStore;
9145
9589
  const { classes } = useStyles();
9146
- const { detailsHeight, graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
9590
+ const { graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
9147
9591
  const canvasScrollContainerRef = React.useRef(null);
9148
9592
  React.useEffect(() => {
9149
9593
  scrollSelectedFeatureIntoView(model, canvasScrollContainerRef);
9150
9594
  }, [model, selectedFeature]);
9151
9595
  const onDetailsResize = (delta) => {
9152
- model.setDetailsHeight(detailsHeight - delta);
9596
+ model.setDetailsHeight(model.detailsHeight - delta);
9153
9597
  };
9154
9598
  if (!ontologyStore) {
9155
9599
  return (React__default["default"].createElement("div", { className: classes.alertContainer },
9156
9600
  React__default["default"].createElement(material.Alert, { severity: "error" }, "Could not load feature type ontology.")));
9157
9601
  }
9158
9602
  if (graphical && table) {
9159
- const tabularHeight = tabularEditor.isShown ? detailsHeight : 0;
9603
+ const tabularHeight = tabularEditor.isShown ? model.detailsHeight : 0;
9160
9604
  const featureAreaHeight = isShown
9161
- ? overallHeight - detailsHeight - accordionControlHeight * 2
9605
+ ? overallHeight - model.detailsHeight - accordionControlHeight * 2
9162
9606
  : 0;
9163
9607
  return (React__default["default"].createElement("div", { style: { height: overallHeight } },
9164
9608
  React__default["default"].createElement(AccordionControl, { open: isShown, title: "Graphical", onClick: toggleShown }),
@@ -10703,7 +11147,8 @@ function stateModelFactory(pluginManager, configSchema) {
10703
11147
  return start1 - start2 || end1 - end2;
10704
11148
  })) {
10705
11149
  for (const [, childFeature] of feature.children ?? new Map()) {
10706
- if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript')) {
11150
+ if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript') ||
11151
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) {
10707
11152
  for (const [, grandChildFeature] of childFeature.children ||
10708
11153
  new Map()) {
10709
11154
  let startingRow;