@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
package/dist/index.esm.js CHANGED
@@ -1,25 +1,26 @@
1
1
  import { checkRegistry, changeRegistry, Change, isAssemblySpecificChange } from '@apollo-annotation/common';
2
- import { gff3ToAnnotationFeature, AddAssemblyFromExternalChange, AddAssemblyAndFeaturesFromFileChange, AddAssemblyFromFileChange, AddFeatureChange, DeleteAssemblyChange, DeleteFeatureChange, annotationFeatureToGFF3, AddFeaturesFromFileChange, UserChange, DeleteUserChange, FeatureAttributeChange, AddRefSeqAliasesChange, getDecodedToken, makeUserSessionId, LocationEndChange, LocationStartChange, TypeChange, StrandChange, splitStringIntoChunks, validationRegistry, ValidationResultSet, filterJBrowseConfig, ImportJBrowseConfigChange, changes, CDSCheck, CoreValidation, ParentChildValidation } from '@apollo-annotation/shared';
2
+ import { gff3ToAnnotationFeature, AddAssemblyFromExternalChange, AddAssemblyAndFeaturesFromFileChange, AddAssemblyFromFileChange, AddFeatureChange, DeleteAssemblyChange, DeleteFeatureChange, annotationFeatureToGFF3, AddFeaturesFromFileChange, UserChange, DeleteUserChange, AddRefSeqAliasesChange, getDecodedToken, makeUserSessionId, LocationEndChange, LocationStartChange, FeatureAttributeChange, TypeChange, StrandChange, splitStringIntoChunks, validationRegistry, ValidationResultSet, filterJBrowseConfig, ImportJBrowseConfigChange, changes, CDSCheck, CoreValidation, ParentChildValidation } from '@apollo-annotation/shared';
3
3
  import { ConfigurationSchema, readConfObject, getConf, ConfigurationReference } from '@jbrowse/core/configuration';
4
4
  import { BaseInternetAccountConfig, InternetAccount, RendererType, TextSearchAdapterType, BaseDisplay, WidgetType, createBaseTrackConfig, TrackType, createBaseTrackModel, InternetAccountType, DisplayType } from '@jbrowse/core/pluggableElementTypes';
5
5
  import Plugin from '@jbrowse/core/Plugin';
6
- import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getSession, getContainingView, getFrame, revcom, defaultCodonTable, isSessionModelWithWidgets, intersection2, doesIntersect2, reverse, defaultStarts, defaultStops } from '@jbrowse/core/util';
6
+ import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getSession, getContainingView, revcom, defaultCodonTable, isSessionModelWithWidgets, getFrame, intersection2, doesIntersect2, reverse, defaultStarts, defaultStops } from '@jbrowse/core/util';
7
7
  import AddIcon from '@mui/icons-material/Add';
8
8
  import { autorun, toJS, observable } from 'mobx';
9
9
  import { getSnapshot, getParent, getRoot, types, addDisposer, flow, cast, isAlive, resolveIdentifier, getParentOfType, applySnapshot } from 'mobx-state-tree';
10
10
  import { io } from 'socket.io-client';
11
11
  import gff from '@gmod/gff';
12
- import LinkIcon from '@mui/icons-material/Link';
13
- import { DialogTitle, IconButton, DialogContent, DialogContentText, Select, MenuItem, TextField, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio, Box, Typography, FormGroup, Checkbox, DialogActions, Button, Autocomplete, InputLabel, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Grid2, Tooltip, Chip, useTheme, FormHelperText, SvgIcon, Divider, Menu, InputAdornment as InputAdornment$1, alpha, CircularProgress, Alert, Avatar } from '@mui/material';
14
- import InputAdornment from '@mui/material/InputAdornment';
15
- import LinearProgress from '@mui/material/LinearProgress';
12
+ import { DialogTitle, IconButton, DialogContent, LinearProgress, TextField, Accordion, AccordionSummary, Typography, AccordionDetails, FormGroup, FormControlLabel, Checkbox, Box, Tooltip, Table, TableBody, TableRow, TableCell, InputAdornment, DialogActions, Button, DialogContentText, Autocomplete, FormControl, InputLabel, Select, MenuItem, TableContainer, Paper, TableHead, useTheme, FormHelperText, Grid2, SvgIcon, Divider, Menu, Chip, FormLabel, RadioGroup, Radio, alpha, CircularProgress, Alert, Avatar } from '@mui/material';
13
+ import { makeStyles } from 'tss-react/mui';
16
14
  import ObjectID from 'bson-objectid';
17
15
  import * as React from 'react';
18
- import React__default, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
16
+ import React__default, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
19
17
  import { Dialog as Dialog$1, Menu as Menu$1 } from '@jbrowse/core/ui';
20
18
  import CloseIcon from '@mui/icons-material/Close';
21
19
  import { observer } from 'mobx-react';
22
- import { makeStyles } from 'tss-react/mui';
20
+ import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
21
+ import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
22
+ import InfoIcon from '@mui/icons-material/Info';
23
+ import LinkIcon from '@mui/icons-material/Link';
23
24
  import { LocalPathLocation, UriLocation, BlobLocation, ElementId } from '@jbrowse/core/util/types/mst';
24
25
  import { openDB, deleteDB } from 'idb/with-async-ittr';
25
26
  import { checkAbortSignal, isAbortException } from '@jbrowse/core/util/aborting';
@@ -29,11 +30,9 @@ import equal from 'fast-deep-equal/es6';
29
30
  import { saveAs } from 'file-saver';
30
31
  import Checkbox$1 from '@mui/material/Checkbox';
31
32
  import FormControlLabel$1 from '@mui/material/FormControlLabel';
33
+ import LinearProgress$1 from '@mui/material/LinearProgress';
32
34
  import DeleteIcon from '@mui/icons-material/Delete';
33
35
  import { DataGrid, GridToolbar, GridActionsCellItem } from '@mui/x-data-grid';
34
- import { debounce } from '@mui/material/utils';
35
- import highlightMatch from 'autosuggest-highlight/match';
36
- import highlightParse from 'autosuggest-highlight/parse';
37
36
  import { nanoid } from 'nanoid';
38
37
  import AccountCircleIcon from '@mui/icons-material/AccountCircle';
39
38
  import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType';
@@ -41,16 +40,23 @@ import { BaseSequenceAdapter, BaseAdapter } from '@jbrowse/core/data_adapters/Ba
41
40
  import { ObservableCreate } from '@jbrowse/core/util/rxjs';
42
41
  import SimpleFeature from '@jbrowse/core/util/simpleFeature';
43
42
  import BaseResult from '@jbrowse/core/TextSearch/BaseResults';
43
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
44
+ import { debounce } from '@mui/material/utils';
45
+ import highlightMatch from 'autosuggest-highlight/match';
46
+ import highlightParse from 'autosuggest-highlight/parse';
44
47
  import { AnnotationFeatureModel, ApolloAssembly, CheckResult, ApolloRefSeq } from '@apollo-annotation/mst';
48
+ import styled from '@emotion/styled';
49
+ import RemoveIcon from '@mui/icons-material/Remove';
50
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
51
+ import ContentCutIcon from '@mui/icons-material/ContentCut';
45
52
  import ClearIcon from '@mui/icons-material/Clear';
46
53
  import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
47
54
  import { getParentRenderProps } from '@jbrowse/core/util/tracks';
48
55
  import ExpandLessIcon from '@mui/icons-material/ExpandLess';
49
- import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
50
56
  import ErrorIcon from '@mui/icons-material/Error';
51
57
  import SaveIcon from '@mui/icons-material/Save';
52
58
 
53
- var version = "0.3.4";
59
+ var version = "0.3.5";
54
60
 
55
61
  const ApolloConfigSchema = ConfigurationSchema('ApolloInternetAccount', {
56
62
  baseURL: {
@@ -130,6 +136,55 @@ async function checkFeatures(assembly) {
130
136
  return checkResults;
131
137
  }
132
138
 
139
+ function getFeatureName(feature) {
140
+ const { attributes } = feature;
141
+ const name = attributes.get('gff_name');
142
+ if (name) {
143
+ return name[0];
144
+ }
145
+ return '';
146
+ }
147
+ function getFeatureId$1(feature) {
148
+ const { attributes } = feature;
149
+ const id = attributes.get('gff_id');
150
+ const transcript_id = attributes.get('transcript_id');
151
+ const exon_id = attributes.get('exon_id');
152
+ const protein_id = attributes.get('protein_id');
153
+ if (id) {
154
+ return id[0];
155
+ }
156
+ if (transcript_id) {
157
+ return transcript_id[0];
158
+ }
159
+ if (exon_id) {
160
+ return exon_id[0];
161
+ }
162
+ if (protein_id) {
163
+ return protein_id[0];
164
+ }
165
+ return '';
166
+ }
167
+ function getFeatureNameOrId$1(feature) {
168
+ const name = getFeatureName(feature);
169
+ const id = getFeatureId$1(feature);
170
+ if (name) {
171
+ return `: ${name}`;
172
+ }
173
+ if (id) {
174
+ return `: ${id}`;
175
+ }
176
+ return '';
177
+ }
178
+ function getStrand(strand) {
179
+ if (strand === 1) {
180
+ return 'Forward';
181
+ }
182
+ if (strand === -1) {
183
+ return 'Reverse';
184
+ }
185
+ return '';
186
+ }
187
+
133
188
  async function createFetchErrorMessage(response, additionalText) {
134
189
  let errorMessage;
135
190
  try {
@@ -170,14 +225,59 @@ const Dialog = observer(function JBrowseDialog(props) {
170
225
  React__default.createElement(CloseIcon, null))) }));
171
226
  });
172
227
 
173
- /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
228
+ /* eslint-disable @typescript-eslint/unbound-method */
174
229
  var FileType;
175
230
  (function (FileType) {
176
231
  FileType["GFF3"] = "text/x-gff3";
177
232
  FileType["FASTA"] = "text/x-fasta";
233
+ FileType["BGZIP_FASTA"] = "application/x-bgzip-fasta";
234
+ FileType["FAI"] = "text/x-fai";
235
+ FileType["GZI"] = "application/x-gzi";
178
236
  FileType["EXTERNAL"] = "text/x-external";
179
237
  })(FileType || (FileType = {}));
238
+ const useStyles$e = makeStyles()((theme) => ({
239
+ accordion: {
240
+ border: `1px solid ${theme.palette.divider}`,
241
+ '&:not(:last-child)': {
242
+ borderBottom: 0,
243
+ },
244
+ },
245
+ accordionSummary: {
246
+ flexDirection: 'row-reverse',
247
+ },
248
+ accordionDetails: {
249
+ padding: theme.spacing(2),
250
+ borderTop: '1px solid rgba(0, 0, 0, .125)',
251
+ },
252
+ radioIcon: {
253
+ color: theme?.palette?.tertiary?.contrastText,
254
+ },
255
+ dialog: {
256
+ // minHeight: 500,
257
+ minWidth: 550,
258
+ maxWidth: 800,
259
+ },
260
+ }));
261
+ function checkSumbission(validAsm, sequenceIsEditable, fileType, fastaFile, fastaIndexFile, fastaGziIndexFile, validFastaUrl, validFastaIndexUrl, validFastaGziIndexUrl) {
262
+ if (!validAsm) {
263
+ return false;
264
+ }
265
+ if (sequenceIsEditable && fastaFile) {
266
+ return true;
267
+ }
268
+ if (fileType === FileType.GFF3 && fastaFile) {
269
+ return true;
270
+ }
271
+ if (fastaFile && fastaIndexFile && fastaGziIndexFile) {
272
+ return true;
273
+ }
274
+ if (validFastaUrl && validFastaIndexUrl && validFastaGziIndexUrl) {
275
+ return true;
276
+ }
277
+ return false;
278
+ }
180
279
  function AddAssembly({ changeManager, handleClose, session, }) {
280
+ const { classes } = useStyles$e();
181
281
  const { internetAccounts } = getRoot(session);
182
282
  const { notify } = session;
183
283
  const apolloInternetAccounts = internetAccounts.filter((ia) => ia.type === 'ApolloInternetAccount');
@@ -187,50 +287,32 @@ function AddAssembly({ changeManager, handleClose, session, }) {
187
287
  const [assemblyName, setAssemblyName] = useState('');
188
288
  const [errorMessage, setErrorMessage] = useState('');
189
289
  const [validAsm, setValidAsm] = useState(false);
190
- const [file, setFile] = useState(null);
191
- const [fileType, setFileType] = useState(FileType.GFF3);
290
+ const [fileType, setFileType] = useState(FileType.BGZIP_FASTA);
192
291
  const [importFeatures, setImportFeatures] = useState(true);
292
+ const [sequenceIsEditable, setSequenceIsEditable] = useState(false);
193
293
  const [submitted, setSubmitted] = useState(false);
194
- const [selectedInternetAccount, setSelectedInternetAccount] = useState(apolloInternetAccounts[0]);
195
- const [fastaFile, setFastaFile] = useState('');
196
- const [fastaIndexFile, setFastaIndexFile] = useState('');
197
- const [fastaGziIndexFile, setFastaGziIndexFile] = useState('');
294
+ const [fastaFile, setFastaFile] = useState(null);
295
+ const [fastaIndexFile, setFastaIndexFile] = useState(null);
296
+ const [fastaGziIndexFile, setFastaGziIndexFile] = useState(null);
297
+ const [fastaUrl, setFastaUrl] = useState('');
298
+ const [fastaIndexUrl, setFastaIndexUrl] = useState('');
299
+ const [fastaGziIndexUrl, setFastaGziIndexUrl] = useState('');
198
300
  const [loading, setLoading] = useState(false);
199
- function handleChangeInternetAccount(e) {
200
- setSubmitted(false);
201
- const newlySelectedInternetAccount = apolloInternetAccounts.find((ia) => ia.internetAccountId === e.target.value);
202
- if (!newlySelectedInternetAccount) {
203
- throw new Error(`Could not find internetAccount with ID "${e.target.value}"`);
204
- }
205
- setSelectedInternetAccount(newlySelectedInternetAccount);
206
- }
207
- function handleChangeFile(e) {
208
- if (!e.target.files) {
209
- return;
210
- }
211
- const selectedFile = e.target.files.item(0);
212
- setFile(selectedFile);
213
- if (!selectedFile) {
214
- return;
215
- }
216
- const fileNameLower = selectedFile.name.toLowerCase();
217
- if (fileNameLower.endsWith('.fasta') ||
218
- fileNameLower.endsWith('.fna') ||
219
- fileNameLower.endsWith('.fa')) {
220
- setFileType(FileType.FASTA);
301
+ const [isGzip, setIsGzip] = useState(false);
302
+ useEffect(() => {
303
+ setFastaIndexUrl(fastaUrl ? `${fastaUrl}.fai` : '');
304
+ }, [fastaUrl]);
305
+ useEffect(() => {
306
+ setFastaGziIndexUrl(fastaUrl ? `${fastaUrl}.gzi` : '');
307
+ }, [fastaUrl]);
308
+ useEffect(() => {
309
+ if (sequenceIsEditable || fileType === FileType.GFF3) {
310
+ setIsGzip(fastaFile?.name.toLocaleLowerCase().endsWith('.gz') ? true : false);
221
311
  }
222
- else if (fileNameLower.endsWith('.gff3') ||
223
- fileNameLower.endsWith('.gff')) {
224
- setFileType(FileType.GFF3);
312
+ else {
313
+ setIsGzip(true);
225
314
  }
226
- }
227
- function handleChangeFileType(e) {
228
- setFileType(e.target.value);
229
- setImportFeatures(e.target.value === FileType.GFF3);
230
- setFastaFile('');
231
- setFastaIndexFile('');
232
- setFile(null);
233
- }
315
+ }, [fastaFile, sequenceIsEditable, fileType]);
234
316
  function checkAssemblyName(assembly) {
235
317
  const { assemblies } = session;
236
318
  const checkAsm = assemblies.find((asm) => readConfObject(asm, 'displayName') === assembly);
@@ -243,6 +325,61 @@ function AddAssembly({ changeManager, handleClose, session, }) {
243
325
  setErrorMessage('');
244
326
  }
245
327
  }
328
+ async function uploadFile(file, fileType) {
329
+ const { jobsManager } = session;
330
+ const controller = new AbortController();
331
+ const [{ baseURL, getFetcher }] = apolloInternetAccounts;
332
+ const url = new URL('files', baseURL);
333
+ url.searchParams.set('type', fileType);
334
+ const uri = url.href;
335
+ const formData = new FormData();
336
+ let filename = file.name;
337
+ if (fileType === FileType.FAI || fileType === FileType.GZI) {
338
+ filename = `${filename}.txt`;
339
+ }
340
+ else if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
341
+ filename = `${filename}.gz`;
342
+ }
343
+ else if (!isGzip && file.name.toLocaleLowerCase().endsWith('.gz')) {
344
+ filename = `${filename}.txt`;
345
+ }
346
+ formData.append('file', file, filename);
347
+ formData.append('type', fileType);
348
+ const apolloFetchFile = getFetcher({
349
+ locationType: 'UriLocation',
350
+ uri,
351
+ });
352
+ if (apolloFetchFile) {
353
+ const job = {
354
+ name: `UploadAssemblyFile for ${assemblyName}`,
355
+ statusMessage: 'Pre-validating',
356
+ progressPct: 0,
357
+ cancelCallback: () => {
358
+ controller.abort();
359
+ jobsManager.abortJob(job.name);
360
+ },
361
+ };
362
+ jobsManager.runJob(job);
363
+ jobsManager.update(job.name, `Uploading ${file.name}, this may take awhile`);
364
+ const { signal } = controller;
365
+ const response = await apolloFetchFile(uri, {
366
+ method: 'POST',
367
+ body: formData,
368
+ signal,
369
+ });
370
+ if (!response.ok) {
371
+ const newErrorMessage = await createFetchErrorMessage(response, 'Error when inserting new assembly (while uploading file)');
372
+ jobsManager.abortJob(job.name, newErrorMessage);
373
+ setErrorMessage(newErrorMessage);
374
+ return '';
375
+ }
376
+ const result = await response.json();
377
+ const fileId = result._id;
378
+ jobsManager.done(job);
379
+ return fileId;
380
+ }
381
+ throw new Error('Failed to fetch');
382
+ }
246
383
  async function onSubmit(event) {
247
384
  event.preventDefault();
248
385
  setErrorMessage('');
@@ -251,51 +388,6 @@ function AddAssembly({ changeManager, handleClose, session, }) {
251
388
  notify(`Assembly "${assemblyName}" is being added`, 'info');
252
389
  handleClose();
253
390
  event.preventDefault();
254
- const { jobsManager } = session;
255
- const controller = new AbortController();
256
- const job = {
257
- name: `UploadAssemblyFile for ${assemblyName}`,
258
- statusMessage: 'Pre-validating',
259
- progressPct: 0,
260
- cancelCallback: () => {
261
- controller.abort();
262
- jobsManager.abortJob(job.name);
263
- },
264
- };
265
- jobsManager.runJob(job);
266
- let fileId = '';
267
- const { baseURL, getFetcher, internetAccountId } = selectedInternetAccount;
268
- if (fileType !== FileType.EXTERNAL && file) {
269
- // First upload file
270
- const url = new URL('files', baseURL);
271
- url.searchParams.set('type', fileType);
272
- const uri = url.href;
273
- const formData = new FormData();
274
- formData.append('file', file);
275
- formData.append('fileName', file.name);
276
- formData.append('type', fileType);
277
- const apolloFetchFile = getFetcher({
278
- locationType: 'UriLocation',
279
- uri,
280
- });
281
- if (apolloFetchFile) {
282
- jobsManager.update(job.name, 'Uploading file, this may take awhile');
283
- const { signal } = controller;
284
- const response = await apolloFetchFile(uri, {
285
- method: 'POST',
286
- body: formData,
287
- signal,
288
- });
289
- if (!response.ok) {
290
- const newErrorMessage = await createFetchErrorMessage(response, 'Error when inserting new assembly (while uploading file)');
291
- jobsManager.abortJob(job.name, newErrorMessage);
292
- setErrorMessage(newErrorMessage);
293
- return;
294
- }
295
- const result = await response.json();
296
- fileId = result._id;
297
- }
298
- }
299
391
  let change;
300
392
  if (fileType === FileType.EXTERNAL) {
301
393
  change = new AddAssemblyFromExternalChange({
@@ -303,30 +395,67 @@ function AddAssembly({ changeManager, handleClose, session, }) {
303
395
  assembly: new ObjectID().toHexString(),
304
396
  assemblyName,
305
397
  externalLocation: {
306
- fa: fastaFile,
307
- fai: fastaIndexFile,
308
- ...(fastaGziIndexFile ? { gzi: fastaGziIndexFile } : {}),
398
+ fa: fastaUrl,
399
+ fai: fastaIndexUrl,
400
+ gzi: fastaGziIndexUrl,
309
401
  },
310
402
  });
311
403
  }
312
404
  else {
313
- const fileUploadChangeBase = {
314
- assembly: new ObjectID().toHexString(),
315
- assemblyName,
316
- fileIds: { fa: fileId },
317
- };
318
- change =
319
- fileType === FileType.GFF3 && importFeatures
320
- ? new AddAssemblyAndFeaturesFromFileChange({
321
- typeName: 'AddAssemblyAndFeaturesFromFileChange',
322
- ...fileUploadChangeBase,
323
- })
324
- : new AddAssemblyFromFileChange({
325
- typeName: 'AddAssemblyFromFileChange',
326
- ...fileUploadChangeBase,
327
- });
405
+ if (!fastaFile) {
406
+ throw new Error('Missing fasta file');
407
+ }
408
+ if (fileType === FileType.GFF3 && importFeatures) {
409
+ const faId = await uploadFile(fastaFile, FileType.GFF3);
410
+ change = new AddAssemblyAndFeaturesFromFileChange({
411
+ typeName: 'AddAssemblyAndFeaturesFromFileChange',
412
+ assembly: new ObjectID().toHexString(),
413
+ assemblyName,
414
+ fileIds: { fa: faId },
415
+ });
416
+ }
417
+ else if (fileType === FileType.GFF3) {
418
+ const faId = await uploadFile(fastaFile, FileType.GFF3);
419
+ change = new AddAssemblyFromFileChange({
420
+ typeName: 'AddAssemblyFromFileChange',
421
+ assembly: new ObjectID().toHexString(),
422
+ assemblyName,
423
+ fileIds: {
424
+ fa: faId,
425
+ },
426
+ });
427
+ }
428
+ else if (sequenceIsEditable) {
429
+ const faId = await uploadFile(fastaFile, FileType.FASTA);
430
+ change = new AddAssemblyFromFileChange({
431
+ typeName: 'AddAssemblyFromFileChange',
432
+ assembly: new ObjectID().toHexString(),
433
+ assemblyName,
434
+ fileIds: {
435
+ fa: faId,
436
+ },
437
+ });
438
+ }
439
+ else {
440
+ if (!fastaIndexFile || !fastaGziIndexFile) {
441
+ throw new Error('Missing fasta index files');
442
+ }
443
+ const faId = await uploadFile(fastaFile, FileType.BGZIP_FASTA);
444
+ const faiId = await uploadFile(fastaIndexFile, FileType.FAI);
445
+ const gziId = await uploadFile(fastaGziIndexFile, FileType.GZI);
446
+ change = new AddAssemblyFromFileChange({
447
+ typeName: 'AddAssemblyFromFileChange',
448
+ assembly: new ObjectID().toHexString(),
449
+ assemblyName,
450
+ fileIds: {
451
+ fa: faId,
452
+ fai: faiId,
453
+ gzi: gziId,
454
+ },
455
+ });
456
+ }
328
457
  }
329
- jobsManager.done(job);
458
+ const [{ internetAccountId }] = apolloInternetAccounts;
330
459
  await changeManager.submit(change, {
331
460
  internetAccountId,
332
461
  updateJobsManager: true,
@@ -334,89 +463,175 @@ function AddAssembly({ changeManager, handleClose, session, }) {
334
463
  setSubmitted(false);
335
464
  setLoading(false);
336
465
  }
337
- let validFastaFile = false;
466
+ let validFastaUrl = false;
338
467
  try {
339
- const url = new URL(fastaFile);
468
+ const url = new URL(fastaUrl);
340
469
  if (url.protocol === 'http:' || url.protocol === 'https:') {
341
- validFastaFile = true;
470
+ validFastaUrl = true;
342
471
  }
343
472
  }
344
473
  catch {
345
474
  // pass
346
475
  }
347
- let validFastaIndexFile = false;
476
+ let validFastaIndexUrl = false;
348
477
  try {
349
- const url = new URL(fastaIndexFile);
478
+ const url = new URL(fastaIndexUrl);
350
479
  if (url.protocol === 'http:' || url.protocol === 'https:') {
351
- validFastaIndexFile = true;
480
+ validFastaIndexUrl = true;
352
481
  }
353
482
  }
354
483
  catch {
355
484
  // pass
356
485
  }
357
- let validFastaGziIndexFile = false;
486
+ let validFastaGziIndexUrl = false;
358
487
  try {
359
- const url = new URL(fastaGziIndexFile);
488
+ const url = new URL(fastaGziIndexUrl);
360
489
  if (url.protocol === 'http:' || url.protocol === 'https:') {
361
- validFastaGziIndexFile = true;
490
+ validFastaGziIndexUrl = true;
362
491
  }
363
492
  }
364
493
  catch {
365
494
  // pass
366
495
  }
367
- return (React__default.createElement(Dialog, { open: true, maxWidth: false, "data-testid": "add-assembly-dialog", title: "Add new assembly", handleClose: handleClose },
368
- loading ? React__default.createElement(LinearProgress, null) : null,
369
- React__default.createElement("form", { onSubmit: onSubmit },
370
- React__default.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
371
- apolloInternetAccounts.length > 1 ? (React__default.createElement(React__default.Fragment, null,
372
- React__default.createElement(DialogContentText, null, "Select account"),
373
- React__default.createElement(Select, { value: selectedInternetAccount.internetAccountId, onChange: handleChangeInternetAccount, disabled: submitted && !errorMessage }, internetAccounts.map((option) => (React__default.createElement(MenuItem, { key: option.id, value: option.internetAccountId }, option.name)))))) : null,
496
+ const [expanded, setExpanded] = React__default.useState('panelFastaInput');
497
+ const handleAccordionChange = (panel) => (event, newExpanded) => {
498
+ if (newExpanded) {
499
+ setExpanded(panel);
500
+ }
501
+ if (panel === 'panelGffInput') {
502
+ setIsGzip(false);
503
+ }
504
+ else {
505
+ setIsGzip(true);
506
+ }
507
+ };
508
+ return (React__default.createElement(Dialog, { open: true, handleClose: handleClose, "data-testid": "add-assembly-dialog", title: "Add new assembly", maxWidth: false },
509
+ React__default.createElement("form", { onSubmit: onSubmit, "data-testid": "submit-form" },
510
+ React__default.createElement(DialogContent, { className: classes.dialog },
511
+ loading ? React__default.createElement(LinearProgress, null) : null,
374
512
  React__default.createElement(TextField, { margin: "dense", id: "name", label: "Assembly name", type: "TextField", fullWidth: true, variant: "outlined", onChange: (e) => {
375
513
  setSubmitted(false);
376
514
  setAssemblyName(e.target.value);
377
515
  checkAssemblyName(e.target.value);
378
516
  }, disabled: submitted && !errorMessage }),
379
- React__default.createElement(FormControl, { style: { marginTop: 20 } },
380
- React__default.createElement(FormLabel, null, "Select GFF3, FASTA or EXTERNAL option"),
381
- React__default.createElement(RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: FileType.GFF3, name: "radio-buttons-group", onChange: handleChangeFileType, value: fileType },
382
- React__default.createElement(FormControlLabel, { value: FileType.GFF3, control: React__default.createElement(Radio, null), label: "GFF3", disabled: submitted && !errorMessage }),
383
- React__default.createElement(FormControlLabel, { value: FileType.FASTA, control: React__default.createElement(Radio, null), label: "FASTA", disabled: submitted && !errorMessage }),
384
- React__default.createElement(FormControlLabel, { value: FileType.EXTERNAL, control: React__default.createElement(Radio, null), label: "External", disabled: submitted && !errorMessage }))),
385
- fileType === FileType.EXTERNAL ? (React__default.createElement(Box, { style: { marginTop: 20 } },
386
- React__default.createElement(Typography, { variant: "caption" }, "Enter FASTA and FASTA index(es) URL"),
387
- React__default.createElement(TextField, { margin: "dense", helperText: "Can be bgz-compressed", id: "fasta", label: "FASTA", type: "TextField", fullWidth: true, variant: "outlined", error: !validFastaFile, onChange: (e) => {
388
- setFastaFile(e.target.value);
389
- }, disabled: submitted && !errorMessage, InputProps: {
390
- startAdornment: (React__default.createElement(InputAdornment, { position: "start" },
391
- React__default.createElement(LinkIcon, null))),
392
- } }),
393
- React__default.createElement(TextField, { margin: "dense", id: "fasta-index", label: "FASTA Index", helperText: ".fai or .gz.fai", type: "TextField", fullWidth: true, variant: "outlined", error: !validFastaIndexFile, onChange: (e) => {
394
- setFastaIndexFile(e.target.value);
395
- }, disabled: submitted && !errorMessage, InputProps: {
396
- startAdornment: (React__default.createElement(InputAdornment, { position: "start" },
397
- React__default.createElement(LinkIcon, null))),
398
- } }),
399
- React__default.createElement(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) => {
400
- setFastaGziIndexFile(e.target.value);
401
- }, disabled: submitted && !errorMessage, InputProps: {
402
- startAdornment: (React__default.createElement(InputAdornment, { position: "start" },
403
- React__default.createElement(LinkIcon, null))),
404
- } }))) : (React__default.createElement(Box, { style: { marginTop: 20 } },
405
- React__default.createElement("input", { type: "file", onChange: handleChangeFile, disabled: submitted && !errorMessage }),
406
- React__default.createElement(FormGroup, null,
407
- React__default.createElement(FormControlLabel, { control: React__default.createElement(Checkbox, { checked: fileType === FileType.GFF3 && importFeatures, onChange: () => {
408
- setImportFeatures(!importFeatures);
409
- }, disabled: fileType !== FileType.GFF3 ||
410
- (submitted && !errorMessage) }), label: "Also load features from GFF3 file" }))))),
517
+ React__default.createElement(Accordion, { disableGutters: true, elevation: 0, square: true, className: classes.accordion, expanded: expanded === 'panelFastaInput', onChange: handleAccordionChange('panelFastaInput') },
518
+ React__default.createElement(AccordionSummary, { className: classes.accordionSummary, expandIcon: expanded === 'panelFastaInput' ? (React__default.createElement(RadioButtonCheckedIcon, { className: classes.radioIcon, sx: { fontSize: '1.2rem', ml: 5 } })) : (React__default.createElement(RadioButtonUncheckedIcon, { className: classes.radioIcon, sx: { fontSize: '1.2rem', mr: 5 } })), "aria-controls": "panelFastaInputd-content", id: "panelFastaInputd-header" },
519
+ React__default.createElement(Typography, { component: "span" }, "FASTA input")),
520
+ React__default.createElement(AccordionDetails, { className: classes.accordionDetails },
521
+ React__default.createElement(FormGroup, null,
522
+ React__default.createElement(FormControlLabel, { "data-testid": "files-on-url-checkbox", control: React__default.createElement(Checkbox, { onChange: () => {
523
+ setFileType(fileType === FileType.EXTERNAL
524
+ ? FileType.BGZIP_FASTA
525
+ : FileType.EXTERNAL);
526
+ if (fileType === FileType.EXTERNAL) {
527
+ setSequenceIsEditable(false);
528
+ }
529
+ }, checked: fileType === FileType.EXTERNAL, disabled: sequenceIsEditable && fileType !== FileType.GFF3 }), label: React__default.createElement(Box, { display: "flex", alignItems: "center" },
530
+ "Use external URLs",
531
+ React__default.createElement(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" },
532
+ React__default.createElement(IconButton, { size: "small" },
533
+ React__default.createElement(InfoIcon, { sx: { fontSize: 18 } })))) }),
534
+ React__default.createElement(FormControlLabel, { "data-testid": "sequence-is-editable-checkbox", control: React__default.createElement(Checkbox, { onChange: () => {
535
+ setSequenceIsEditable(!sequenceIsEditable);
536
+ } }), checked: sequenceIsEditable, disabled: fileType === FileType.EXTERNAL, label: React__default.createElement(Box, { display: "flex", alignItems: "center" },
537
+ "Store sequence in database",
538
+ React__default.createElement(Tooltip, { title: "Enables users to edit the genomic sequence, but comes with performance impacts. Use with care.", placement: "top-start" },
539
+ React__default.createElement(IconButton, { size: "small" },
540
+ React__default.createElement(InfoIcon, { sx: { fontSize: 18 } })))) }),
541
+ React__default.createElement(FormControlLabel, { "data-testid": "fasta-is-gzip-checkbox", control: React__default.createElement(Checkbox, { checked: isGzip, onChange: () => {
542
+ if (sequenceIsEditable) {
543
+ setIsGzip(!isGzip);
544
+ }
545
+ else {
546
+ setIsGzip(true);
547
+ }
548
+ }, disabled: !sequenceIsEditable }), label: "FASTA is gzip compressed" }),
549
+ fileType === FileType.BGZIP_FASTA ||
550
+ fileType === FileType.GFF3 ? (React__default.createElement(Table, { size: "small", sx: { mt: 2 } },
551
+ React__default.createElement(TableBody, null,
552
+ React__default.createElement(TableRow, null),
553
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
554
+ React__default.createElement(Box, { display: "flex", alignItems: "center" },
555
+ React__default.createElement("span", null, "FASTA"),
556
+ React__default.createElement(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.' },
557
+ React__default.createElement(IconButton, { size: "small" },
558
+ React__default.createElement(InfoIcon, { sx: { fontSize: 18 } }))))),
559
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
560
+ React__default.createElement("input", { "data-testid": "fasta-input-file", type: "file", onChange: (e) => {
561
+ setFastaFile(e.target.files?.item(0) ?? null);
562
+ }, disabled: submitted && !errorMessage })),
563
+ React__default.createElement(TableRow, null),
564
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } }, "FASTA index (.fai)"),
565
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
566
+ React__default.createElement("input", { "data-testid": "fai-input-file", type: "file", onChange: (e) => {
567
+ setFastaIndexFile(e.target.files?.item(0) ?? null);
568
+ }, disabled: (submitted && !errorMessage) || sequenceIsEditable })),
569
+ React__default.createElement(TableRow, null),
570
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } }, "FASTA binary index (.gzi)"),
571
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
572
+ React__default.createElement("input", { "data-testid": "gzi-input-file", type: "file", onChange: (e) => {
573
+ setFastaGziIndexFile(e.target.files?.item(0) ?? null);
574
+ }, disabled: (submitted && !errorMessage) || sequenceIsEditable }))))) : (React__default.createElement(Table, { size: "small", sx: { mt: 2 } },
575
+ React__default.createElement(TableBody, null,
576
+ React__default.createElement(TableRow, null),
577
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
578
+ React__default.createElement(Box, { display: "flex", alignItems: "center" },
579
+ React__default.createElement("span", null, "FASTA"),
580
+ React__default.createElement(Tooltip, { title: "Remote FASTA input must be compressed with bgzip and indexed with samtools faidx (or equivalent)" },
581
+ React__default.createElement(IconButton, { size: "small" },
582
+ React__default.createElement(InfoIcon, { sx: { fontSize: 18 } }))))),
583
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
584
+ React__default.createElement(TextField, { "data-testid": "fasta-input-url", variant: "outlined", value: fastaUrl, error: !validFastaUrl, onChange: (e) => {
585
+ setFastaUrl(e.target.value);
586
+ }, disabled: submitted && !errorMessage, slotProps: {
587
+ input: {
588
+ startAdornment: (React__default.createElement(InputAdornment, { position: "start" },
589
+ React__default.createElement(LinkIcon, null))),
590
+ },
591
+ } })),
592
+ React__default.createElement(TableRow, null),
593
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } }, "FASTA index (.fai)"),
594
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
595
+ React__default.createElement(TextField, { "data-testid": "fai-input-url", variant: "outlined", value: fastaIndexUrl, error: !validFastaIndexUrl, onChange: (e) => {
596
+ setFastaIndexUrl(e.target.value);
597
+ }, disabled: submitted && !errorMessage, slotProps: {
598
+ input: {
599
+ startAdornment: (React__default.createElement(InputAdornment, { position: "start" },
600
+ React__default.createElement(LinkIcon, null))),
601
+ },
602
+ } })),
603
+ React__default.createElement(TableRow, null),
604
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } }, "FASTA binary index (.gzi)"),
605
+ React__default.createElement(TableCell, { style: { borderBottomWidth: 0 } },
606
+ React__default.createElement(TextField, { "data-testid": "gzi-input-url", variant: "outlined", value: fastaGziIndexUrl, error: !validFastaGziIndexUrl, onChange: (e) => {
607
+ setFastaGziIndexUrl(e.target.value);
608
+ }, disabled: submitted && !errorMessage, slotProps: {
609
+ input: {
610
+ startAdornment: (React__default.createElement(InputAdornment, { position: "start" },
611
+ React__default.createElement(LinkIcon, null))),
612
+ },
613
+ } })))))))),
614
+ React__default.createElement(Accordion, { disableGutters: true, elevation: 0, square: true, className: classes.accordion, expanded: expanded === 'panelGffInput', onChange: handleAccordionChange('panelGffInput') },
615
+ React__default.createElement(AccordionSummary, { className: classes.accordionSummary, expandIcon: expanded === 'panelGffInput' ? (React__default.createElement(RadioButtonCheckedIcon, { className: classes.radioIcon, sx: { fontSize: '1.2rem', ml: 5 } })) : (React__default.createElement(RadioButtonUncheckedIcon, { className: classes.radioIcon, sx: { fontSize: '1.2rem', mr: 5 } })), "aria-controls": "panelGffInputd-content" },
616
+ React__default.createElement(Typography, { component: "span" },
617
+ "GFF3 input",
618
+ React__default.createElement(Tooltip, { title: "GFF3 must includes FASTA sequences. File can be gzip compressed." },
619
+ React__default.createElement(InfoIcon, { className: classes.radioIcon, sx: { fontSize: 18 } })))),
620
+ React__default.createElement(AccordionDetails, { className: classes.accordionDetails },
621
+ React__default.createElement(Box, { style: { marginTop: 20 } },
622
+ React__default.createElement("input", { "data-testid": "gff3-input-file", type: "file", disabled: submitted && !errorMessage, onChange: (e) => {
623
+ setFastaFile(e.target.files?.item(0) ?? null);
624
+ setFileType(FileType.GFF3);
625
+ } }),
626
+ React__default.createElement(FormGroup, { style: { display: 'grid' } },
627
+ React__default.createElement(FormControlLabel, { control: React__default.createElement(Checkbox, { checked: importFeatures, onChange: () => {
628
+ setImportFeatures(!importFeatures);
629
+ }, disabled: submitted && !errorMessage }), label: "Load features from GFF3 file" }),
630
+ React__default.createElement(FormControlLabel, { "data-testid": "gff3-is-gzip-checkbox", control: React__default.createElement(Checkbox, { checked: isGzip, onChange: () => {
631
+ setIsGzip(!isGzip);
632
+ }, disabled: submitted && !errorMessage }), label: "GFF3 is gzip compressed" })))))),
411
633
  React__default.createElement(DialogActions, null,
412
- React__default.createElement(Button, { disabled: !validAsm ||
413
- !((assemblyName && file) ??
414
- (assemblyName &&
415
- fastaFile &&
416
- fastaIndexFile &&
417
- validFastaFile &&
418
- validFastaIndexFile)) ||
419
- submitted, variant: "contained", type: "submit" }, submitted ? 'Submitting...' : 'Submit'),
634
+ React__default.createElement(Button, { disabled: !checkSumbission(validAsm, sequenceIsEditable, fileType, fastaFile, fastaIndexFile, fastaGziIndexFile, validFastaUrl, validFastaIndexUrl, validFastaGziIndexUrl) || submitted, variant: "contained", type: "submit", "data-testid": "submit-button" }, submitted ? 'Submitting...' : 'Submit'),
420
635
  React__default.createElement(Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
421
636
  errorMessage ? (React__default.createElement(DialogContent, null,
422
637
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
@@ -551,7 +766,16 @@ const genericEnglishStopwords = new Set([
551
766
  'don',
552
767
  'should',
553
768
  'now',
554
- ...'1234567890',
769
+ '0',
770
+ '1',
771
+ '2',
772
+ '3',
773
+ '4',
774
+ '5',
775
+ '6',
776
+ '7',
777
+ '8',
778
+ '9',
555
779
  ]);
556
780
  /**
557
781
  * The set of stopwords we use for fulltext indexing. Currently
@@ -1288,7 +1512,9 @@ const OntologyRecordType = types
1288
1512
  return;
1289
1513
  }
1290
1514
  void self.loadEquivalentTypes('gene');
1515
+ void self.loadEquivalentTypes('pseudogene');
1291
1516
  void self.loadEquivalentTypes('transcript');
1517
+ void self.loadEquivalentTypes('pseudogenic_transcript');
1292
1518
  void self.loadEquivalentTypes('CDS');
1293
1519
  void self.loadEquivalentTypes('mRNA');
1294
1520
  reaction.dispose();
@@ -2258,7 +2484,7 @@ function ImportFeatures({ changeManager, handleClose, session, }) {
2258
2484
  await changeManager.submit(change, { updateJobsManager: true });
2259
2485
  }
2260
2486
  return (React__default.createElement(Dialog, { open: true, title: "Import Features from GFF3 file", handleClose: handleClose, maxWidth: false, "data-testid": "import-features-dialog" },
2261
- loading ? React__default.createElement(LinearProgress, null) : null,
2487
+ loading ? React__default.createElement(LinearProgress$1, null) : null,
2262
2488
  React__default.createElement("form", { onSubmit: onSubmit },
2263
2489
  React__default.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
2264
2490
  React__default.createElement(DialogContentText, null, "Select assembly"),
@@ -2586,462 +2812,50 @@ function ManageUsers({ changeManager, handleClose, session, }) {
2586
2812
  React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
2587
2813
  }
2588
2814
 
2589
- /* eslint-disable @typescript-eslint/unbound-method */
2590
- // interface TermAutocompleteResult extends TermValue {
2591
- // label: string[]
2592
- // match: string
2593
- // category: string[]
2594
- // taxon: string
2595
- // taxon_label: string
2596
- // highlight: string
2597
- // has_highlight: boolean
2598
- // }
2599
- // interface TermAutocompleteResponse {
2600
- // docs: TermAutocompleteResult[]
2601
- // }
2602
- // const hiliteRegex = /(?<=<em class="hilite">)(.*?)(?=<\/em>)/g
2603
- function TermTagWithTooltip({ getTagProps, index, ontology, termId, }) {
2604
- const manager = getParent(ontology, 2);
2605
- const [description, setDescription] = React.useState('');
2606
- const [errorMessage, setErrorMessage] = React.useState('');
2607
- React.useEffect(() => {
2608
- const controller = new AbortController();
2609
- const { signal } = controller;
2610
- async function fetchDescription() {
2611
- const termUrl = manager.expandPrefixes(termId);
2612
- const db = await ontology.dataStore?.db;
2613
- if (!db || signal.aborted) {
2614
- return;
2815
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2816
+ function OpenLocalFile({ handleClose, session }) {
2817
+ const { apolloDataStore } = session;
2818
+ const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
2819
+ const [file, setFile] = useState(null);
2820
+ const [assemblyName, setAssemblyName] = useState('');
2821
+ const [errorMessage, setErrorMessage] = useState('');
2822
+ const [submitted, setSubmitted] = useState(false);
2823
+ const theme = useTheme();
2824
+ function handleChangeFile(e) {
2825
+ const selectedFile = e.target.files?.item(0);
2826
+ if (!selectedFile) {
2827
+ return;
2828
+ }
2829
+ setErrorMessage('');
2830
+ setFile(selectedFile);
2831
+ if (!assemblyName) {
2832
+ const fileName = selectedFile.name;
2833
+ const lastDotIndex = fileName.lastIndexOf('.');
2834
+ if (lastDotIndex === -1) {
2835
+ setAssemblyName(fileName);
2615
2836
  }
2616
- const term = await db
2617
- .transaction('nodes')
2618
- .objectStore('nodes')
2619
- .get(termUrl);
2620
- if (term && term.lbl && !signal.aborted) {
2621
- setDescription(term.lbl || 'no label');
2837
+ else {
2838
+ setAssemblyName(fileName.slice(0, lastDotIndex));
2622
2839
  }
2623
2840
  }
2624
- fetchDescription().catch((error) => {
2625
- if (!signal.aborted) {
2626
- setErrorMessage(String(error));
2627
- }
2628
- });
2629
- return () => {
2630
- controller.abort();
2631
- };
2632
- }, [termId, ontology, manager]);
2633
- return (React.createElement(Tooltip, { title: description },
2634
- React.createElement("div", null,
2635
- React.createElement(Chip, { label: errorMessage || manager.applyPrefixes(termId), color: errorMessage ? 'error' : 'default', size: "small", ...getTagProps({ index }) }))));
2636
- }
2637
- function OntologyTermMultiSelect({ includeDeprecated, onChange, ontologyName, ontologyVersion, session, value: initialValue, }) {
2638
- const { ontologyManager } = session.apolloDataStore;
2639
- const ontology = ontologyManager.findOntology(ontologyName, ontologyVersion);
2640
- const [value, setValue] = React.useState(initialValue.map((id) => ({ term: { id, type: 'CLASS' } })));
2641
- const [inputValue, setInputValue] = React.useState('');
2642
- const [options, setOptions] = React.useState([]);
2643
- const [loading, setLoading] = React.useState(false);
2644
- const [errorMessage, setErrorMessage] = React.useState('');
2645
- const getOntologyTerms = React.useMemo(() => debounce(async (request, callback) => {
2646
- if (!ontology) {
2647
- return;
2841
+ }
2842
+ async function onSubmit(event) {
2843
+ event.preventDefault();
2844
+ setErrorMessage('');
2845
+ setSubmitted(true);
2846
+ if (!file) {
2847
+ throw new Error('No file selected');
2648
2848
  }
2649
- const { dataStore } = ontology;
2650
- if (!dataStore) {
2651
- return;
2652
- }
2653
- const { input, signal } = request;
2654
- try {
2655
- const matches = await dataStore.getTermsByFulltext(input, undefined, signal);
2656
- // aggregate the matches by term
2657
- const byTerm = new Map();
2658
- const options = [];
2659
- for (const match of matches) {
2660
- if (!isOntologyClass(match.term) ||
2661
- (!includeDeprecated && isDeprecated(match.term))) {
2662
- continue;
2663
- }
2664
- let slot = byTerm.get(match.term.id);
2665
- if (!slot) {
2666
- slot = { term: match.term, matches: [] };
2667
- byTerm.set(match.term.id, slot);
2668
- options.push(slot);
2669
- }
2670
- slot.matches.push(match);
2671
- }
2672
- callback(options);
2673
- }
2674
- catch (error) {
2675
- if (!isAbortException(error)) {
2676
- setErrorMessage(String(error));
2677
- }
2678
- }
2679
- }, 400), [includeDeprecated, ontology]);
2680
- React.useEffect(() => {
2681
- const aborter = new AbortController();
2682
- const { signal } = aborter;
2683
- if (inputValue === '') {
2684
- setOptions([]);
2685
- return;
2686
- }
2687
- setLoading(true);
2688
- void getOntologyTerms({ input: inputValue, signal }, (results) => {
2689
- let newOptions = [];
2690
- if (value.length > 0) {
2691
- newOptions = value;
2692
- }
2693
- if (results) {
2694
- newOptions = [...newOptions, ...results];
2695
- }
2696
- setOptions(newOptions);
2697
- setLoading(false);
2698
- });
2699
- return () => {
2700
- aborter.abort();
2701
- };
2702
- }, [getOntologyTerms, ontology, includeDeprecated, inputValue, value]);
2703
- if (!ontology) {
2704
- return null;
2705
- }
2706
- const extraTextFieldParams = {};
2707
- if (errorMessage) {
2708
- extraTextFieldParams.error = true;
2709
- extraTextFieldParams.helperText = errorMessage;
2710
- }
2711
- return (React.createElement(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) ===
2712
- ontologyManager.applyPrefixes(v.term.id), noOptionsText: inputValue ? 'No matches' : 'Start typing to search', onChange: (_, newValue) => {
2713
- setOptions(newValue ? [...newValue, ...options] : options);
2714
- onChange(newValue.map((v) => ontologyManager.applyPrefixes(v.term.id)));
2715
- setValue(newValue);
2716
- }, onInputChange: (event, newInputValue) => {
2717
- if (newInputValue) {
2718
- setLoading(true);
2719
- }
2720
- setOptions([]);
2721
- setInputValue(newInputValue);
2722
- }, multiple: true, renderInput: (params) => (React.createElement(TextField, { ...params, ...extraTextFieldParams, variant: "outlined", fullWidth: true })), renderOption: (props, option) => (React.createElement(Option, { ...props, ontologyManager: ontologyManager, option: option, inputValue: inputValue })), renderTags: (v, getTagProps) => v.map((option, index) => (React.createElement(TermTagWithTooltip, { termId: option.term.id, index: index, ontology: ontology, getTagProps: getTagProps, key: option.term.id }))) }));
2723
- }
2724
- function HighlightedText(props) {
2725
- const { search, str } = props;
2726
- const highlights = highlightMatch(str, search, {
2727
- insideWords: true,
2728
- findAllOccurrences: true,
2729
- });
2730
- const parts = highlightParse(str, highlights);
2731
- return (React.createElement(React.Fragment, null, parts.map((part, index) => (React.createElement(Typography, { key: index, component: "span", sx: { fontWeight: part.highlight ? 'bold' : 'regular' }, variant: "body2", color: "text.secondary" }, part.text)))));
2732
- }
2733
- function Option(props) {
2734
- const { inputValue, ontologyManager, option, ...other } = props;
2735
- const matches = option.matches ?? [];
2736
- const fields = matches
2737
- .filter((match) => match.field.jsonPath !== '$.lbl')
2738
- .map((match) => {
2739
- return (React.createElement(React.Fragment, { key: `option-${match.term.id}-${match.str}` },
2740
- React.createElement(Typography, { component: "dt", variant: "body2", color: "text.secondary" }, match.field.displayName),
2741
- React.createElement("dd", null,
2742
- React.createElement(HighlightedText, { str: match.str, search: inputValue }))));
2743
- });
2744
- // const lblScore = matches
2745
- // .filter((match) => match.field.jsonPath === '$.lbl')
2746
- // .map((m) => m.score)
2747
- // .join(', ')
2748
- return (React.createElement("li", { ...other },
2749
- React.createElement(Grid2, { container: true },
2750
- React.createElement(Grid2, null,
2751
- React.createElement(Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
2752
- ' ',
2753
- React.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
2754
- ' ',
2755
- React.createElement("dl", null, fields)))));
2756
- }
2757
-
2758
- const reservedKeys$1 = new Map([
2759
- [
2760
- 'Gene Ontology',
2761
- (props) => {
2762
- return React__default.createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Gene Ontology" });
2763
- },
2764
- ],
2765
- [
2766
- 'Sequence Ontology',
2767
- (props) => {
2768
- return (React__default.createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Sequence Ontology" }));
2769
- },
2770
- ],
2771
- ]);
2772
- const useStyles$e = makeStyles()((theme) => ({
2773
- attributeInput: {
2774
- maxWidth: 600,
2775
- },
2776
- newAttributePaper: {
2777
- padding: theme.spacing(2),
2778
- },
2779
- attributeName: {
2780
- background: theme.palette.secondary.main,
2781
- color: theme.palette.secondary.contrastText,
2782
- padding: theme.spacing(1),
2783
- },
2784
- }));
2785
- const reservedTerms$1 = [
2786
- 'ID',
2787
- 'Name',
2788
- 'Alias',
2789
- 'Target',
2790
- 'Gap',
2791
- 'Derives_from',
2792
- 'Note',
2793
- 'Dbxref',
2794
- 'Ontology',
2795
- 'Is_Circular',
2796
- ];
2797
- function CustomAttributeValueEditor$1(props) {
2798
- const { onChange, value } = props;
2799
- return (React__default.createElement(TextField, { type: "text", value: value, onChange: (event) => {
2800
- onChange(event.target.value.split(','));
2801
- }, variant: "outlined", fullWidth: true, helperText: "Separate multiple values for the attribute with commas" }));
2802
- }
2803
- function ModifyFeatureAttribute({ changeManager, handleClose, session, sourceAssemblyId, sourceFeature, }) {
2804
- const { notify } = session;
2805
- const { internetAccounts } = getRoot(session);
2806
- const internetAccount = useMemo(() => {
2807
- return internetAccounts.find((ia) => ia.type === 'ApolloInternetAccount');
2808
- }, [internetAccounts]);
2809
- const role = internetAccount ? internetAccount.role : 'admin';
2810
- const editable = ['admin', 'user'].includes(role ?? '');
2811
- const [errorMessage, setErrorMessage] = useState('');
2812
- const [attributes, setAttributes] = useState(Object.fromEntries([...sourceFeature.attributes.entries()].map(([key, value]) => {
2813
- if (key.startsWith('gff_')) {
2814
- const newKey = key.slice(4);
2815
- const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1);
2816
- return [capitalizedKey, getSnapshot(value)];
2817
- }
2818
- if (key === '_id') {
2819
- return ['ID', getSnapshot(value)];
2820
- }
2821
- return [key, getSnapshot(value)];
2822
- })));
2823
- const [showAddNewForm, setShowAddNewForm] = useState(false);
2824
- const [newAttributeKey, setNewAttributeKey] = useState('');
2825
- const { classes } = useStyles$e();
2826
- async function onSubmit(event) {
2827
- event.preventDefault();
2828
- setErrorMessage('');
2829
- const attrs = {};
2830
- if (attributes) {
2831
- for (const [key, val] of Object.entries(attributes)) {
2832
- if (!val) {
2833
- continue;
2834
- }
2835
- const newKey = key.toLowerCase();
2836
- if (newKey === 'parent') {
2837
- continue;
2838
- }
2839
- if ([...reservedKeys$1.keys()].includes(key)) {
2840
- attrs[key] = val;
2841
- continue;
2842
- }
2843
- switch (key) {
2844
- case 'ID': {
2845
- attrs._id = val;
2846
- break;
2847
- }
2848
- case 'Name': {
2849
- attrs.gff_name = val;
2850
- break;
2851
- }
2852
- case 'Alias': {
2853
- attrs.gff_alias = val;
2854
- break;
2855
- }
2856
- case 'Target': {
2857
- attrs.gff_target = val;
2858
- break;
2859
- }
2860
- case 'Gap': {
2861
- attrs.gff_gap = val;
2862
- break;
2863
- }
2864
- case 'Derives_from': {
2865
- attrs.gff_derives_from = val;
2866
- break;
2867
- }
2868
- case 'Note': {
2869
- attrs.gff_note = val;
2870
- break;
2871
- }
2872
- case 'Dbxref': {
2873
- attrs.gff_dbxref = val;
2874
- break;
2875
- }
2876
- case 'Ontology_term': {
2877
- attrs.gff_ontology_term = val;
2878
- break;
2879
- }
2880
- case 'Is_circular': {
2881
- attrs.gff_is_circular = val;
2882
- break;
2883
- }
2884
- default: {
2885
- attrs[key.toLowerCase()] = val;
2886
- }
2887
- }
2888
- }
2889
- }
2890
- const change = new FeatureAttributeChange({
2891
- changedIds: [sourceFeature._id],
2892
- typeName: 'FeatureAttributeChange',
2893
- assembly: sourceAssemblyId,
2894
- featureId: sourceFeature._id,
2895
- attributes: attrs,
2896
- });
2897
- await changeManager.submit(change);
2898
- notify('Feature attributes modified successfully', 'success');
2899
- handleClose();
2900
- event.preventDefault();
2901
- }
2902
- function handleAddNewAttributeChange() {
2903
- setErrorMessage('');
2904
- if (newAttributeKey.trim().length === 0) {
2905
- setErrorMessage('Attribute key is mandatory');
2906
- return;
2907
- }
2908
- if (newAttributeKey === 'Parent') {
2909
- setErrorMessage('"Parent" -key is handled internally and it cannot be modified manually');
2910
- return;
2911
- }
2912
- if (newAttributeKey in attributes) {
2913
- setErrorMessage(`Attribute "${newAttributeKey}" already exists`);
2914
- return;
2915
- }
2916
- if (/^[A-Z]/.test(newAttributeKey) &&
2917
- !reservedTerms$1.includes(newAttributeKey) &&
2918
- ![...reservedKeys$1.keys()].includes(newAttributeKey)) {
2919
- setErrorMessage(`Key cannot starts with uppercase letter unless key is one of these: ${reservedTerms$1.join(', ')}`);
2920
- return;
2921
- }
2922
- setAttributes({ ...attributes, [newAttributeKey]: [] });
2923
- setShowAddNewForm(false);
2924
- setNewAttributeKey('');
2925
- }
2926
- function deleteAttribute(key) {
2927
- setErrorMessage('');
2928
- const { [key]: remove, ...rest } = attributes;
2929
- setAttributes(rest);
2930
- }
2931
- function makeOnChange(id) {
2932
- return (newValue) => {
2933
- setAttributes({ ...attributes, [id]: newValue });
2934
- };
2935
- }
2936
- function handleRadioButtonChange(event, value) {
2937
- if (value === 'custom') {
2938
- setNewAttributeKey('');
2939
- }
2940
- else if (reservedKeys$1.has(value)) {
2941
- setNewAttributeKey(value);
2942
- }
2943
- else {
2944
- setErrorMessage('Unknown attribute type');
2945
- }
2946
- }
2947
- const hasEmptyAttributes = Object.values(attributes).some((value) => value.length === 0 || value.includes(''));
2948
- return (React__default.createElement(Dialog, { open: true, title: "Feature attributes", handleClose: handleClose, maxWidth: false, "data-testid": "modify-feature-attribute" },
2949
- React__default.createElement("form", { onSubmit: onSubmit },
2950
- React__default.createElement(DialogContent, null,
2951
- React__default.createElement(Grid2, { container: true, direction: "column", spacing: 1 },
2952
- Object.entries(attributes).map(([key, value]) => {
2953
- const EditorComponent = reservedKeys$1.get(key) ?? CustomAttributeValueEditor$1;
2954
- return (React__default.createElement(Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
2955
- React__default.createElement(Grid2, null,
2956
- React__default.createElement(Paper, { variant: "outlined", className: classes.attributeName },
2957
- React__default.createElement(Typography, null, key))),
2958
- React__default.createElement(Grid2, { flexGrow: 1 },
2959
- React__default.createElement(EditorComponent, { session: session, value: value, onChange: makeOnChange(key) })),
2960
- React__default.createElement(Grid2, null,
2961
- React__default.createElement(IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
2962
- deleteAttribute(key);
2963
- } },
2964
- React__default.createElement(DeleteIcon, { fontSize: "medium", key: key })))));
2965
- }),
2966
- React__default.createElement(Grid2, null,
2967
- React__default.createElement(Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
2968
- setShowAddNewForm(true);
2969
- } }, "Add new")),
2970
- showAddNewForm ? (React__default.createElement(Grid2, null,
2971
- React__default.createElement(Paper, { elevation: 8, className: classes.newAttributePaper },
2972
- React__default.createElement(Grid2, { container: true, direction: "column" },
2973
- React__default.createElement(Grid2, null,
2974
- React__default.createElement(FormControl, null,
2975
- React__default.createElement(FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
2976
- React__default.createElement(RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
2977
- React__default.createElement(FormControlLabel, { value: "custom", control: React__default.createElement(Radio, null), disableTypography: true, label: React__default.createElement(Grid2, { container: true, spacing: 1, alignItems: "center" },
2978
- React__default.createElement(Grid2, null,
2979
- React__default.createElement(Typography, null, "Custom")),
2980
- React__default.createElement(Grid2, null,
2981
- React__default.createElement(TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys$1.has(newAttributeKey)
2982
- ? ''
2983
- : newAttributeKey, disabled: reservedKeys$1.has(newAttributeKey), onChange: (event) => {
2984
- setNewAttributeKey(event.target.value);
2985
- } }))) }),
2986
- [...reservedKeys$1.keys()].map((key) => (React__default.createElement(FormControlLabel, { key: key, value: key, control: React__default.createElement(Radio, null), label: key })))))),
2987
- React__default.createElement(Grid2, null,
2988
- React__default.createElement(DialogActions, null,
2989
- React__default.createElement(Button, { key: "addButton", color: "primary", variant: "contained", style: { margin: 2 }, onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
2990
- React__default.createElement(Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
2991
- setShowAddNewForm(false);
2992
- setNewAttributeKey('');
2993
- setErrorMessage('');
2994
- } }, "Cancel"))))))) : null),
2995
- errorMessage ? (React__default.createElement(DialogContentText, { color: "error" }, errorMessage)) : null),
2996
- React__default.createElement(DialogActions, null,
2997
- React__default.createElement(Button, { variant: "contained", type: "submit", disabled: showAddNewForm || hasEmptyAttributes || !editable }, "Submit changes"),
2998
- React__default.createElement(Button, { variant: "outlined", type: "submit", disabled: showAddNewForm, onClick: handleClose }, "Cancel")))));
2999
- }
3000
-
3001
- /* eslint-disable @typescript-eslint/no-unsafe-call */
3002
- function OpenLocalFile({ handleClose, session }) {
3003
- const { apolloDataStore } = session;
3004
- const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
3005
- const [file, setFile] = useState(null);
3006
- const [assemblyName, setAssemblyName] = useState('');
3007
- const [errorMessage, setErrorMessage] = useState('');
3008
- const [submitted, setSubmitted] = useState(false);
3009
- const theme = useTheme();
3010
- function handleChangeFile(e) {
3011
- const selectedFile = e.target.files?.item(0);
3012
- if (!selectedFile) {
3013
- return;
3014
- }
3015
- setErrorMessage('');
3016
- setFile(selectedFile);
3017
- if (!assemblyName) {
3018
- const fileName = selectedFile.name;
3019
- const lastDotIndex = fileName.lastIndexOf('.');
3020
- if (lastDotIndex === -1) {
3021
- setAssemblyName(fileName);
3022
- }
3023
- else {
3024
- setAssemblyName(fileName.slice(0, lastDotIndex));
3025
- }
3026
- }
3027
- }
3028
- async function onSubmit(event) {
3029
- event.preventDefault();
3030
- setErrorMessage('');
3031
- setSubmitted(true);
3032
- if (!file) {
3033
- throw new Error('No file selected');
3034
- }
3035
- // Right now we are not using stream because there was a problem with 'pipe' in ReadStream
3036
- const fileData = await new Response(file).text();
3037
- const assemblyId = `${assemblyName}-${file.name}-${nanoid(8)}`;
3038
- try {
3039
- await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore);
3040
- }
3041
- catch (error) {
3042
- console.error(error);
3043
- notify(`Error loading GFF3 ${file.name}, ${String(error)}`, 'error');
3044
- handleClose();
2849
+ // Right now we are not using stream because there was a problem with 'pipe' in ReadStream
2850
+ const fileData = await new Response(file).text();
2851
+ const assemblyId = `${assemblyName}-${file.name}-${nanoid(8)}`;
2852
+ try {
2853
+ await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore);
2854
+ }
2855
+ catch (error) {
2856
+ console.error(error);
2857
+ notify(`Error loading GFF3 ${file.name}, ${String(error)}`, 'error');
2858
+ handleClose();
3045
2859
  return;
3046
2860
  }
3047
2861
  const assemblyConfig = {
@@ -4100,6 +3914,15 @@ class ApolloSequenceAdapter extends BaseSequenceAdapter {
4100
3914
  return;
4101
3915
  }
4102
3916
  const backendDriver = dataStore.getBackendDriver(assemblyId);
3917
+ const regions = await backendDriver.getRegions(regionWithAssemblyName.assemblyName);
3918
+ const region = regions.find((region) => region.refName === regionWithAssemblyName.refName);
3919
+ if (!region) {
3920
+ observer.error('Cannot get region');
3921
+ return;
3922
+ }
3923
+ if (regionWithAssemblyName.end > region.end) {
3924
+ regionWithAssemblyName.end = region.end;
3925
+ }
4103
3926
  const { seq } = await backendDriver.getSequence(regionWithAssemblyName);
4104
3927
  observer.next(new SimpleFeature({
4105
3928
  id: `${refName} ${start}-${end}`,
@@ -4968,16 +4791,52 @@ const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
4968
4791
  throw new Error('featureTypeOntology is undefined');
4969
4792
  }
4970
4793
  return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
4971
- featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
4972
- featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
4794
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript') ||
4795
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogene') ||
4796
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogenic_transcript'));
4797
+ };
4798
+ const isGene = (annotationFeature, apolloSessionModel) => {
4799
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4800
+ if (!featureTypeOntology) {
4801
+ throw new Error('featureTypeOntology is undefined');
4802
+ }
4803
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
4804
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogene'));
4973
4805
  };
4974
4806
  const isTranscript = (annotationFeature, apolloSessionModel) => {
4975
4807
  const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4976
4808
  if (!featureTypeOntology) {
4977
4809
  throw new Error('featureTypeOntology is undefined');
4978
4810
  }
4979
- return (featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
4980
- featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
4811
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript') ||
4812
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogenic_transcript'));
4813
+ };
4814
+ const getFeatureId = (feature) => {
4815
+ const { attributes } = feature;
4816
+ const id = attributes?.id;
4817
+ if (id) {
4818
+ return id[0];
4819
+ }
4820
+ return feature.type;
4821
+ };
4822
+ const getFeatureNameOrId = (feature, apolloSessionModel) => {
4823
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4824
+ if (!featureTypeOntology) {
4825
+ return getFeatureId(feature);
4826
+ }
4827
+ let attrName = '';
4828
+ if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
4829
+ attrName = 'gene_name';
4830
+ }
4831
+ if (featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
4832
+ attrName = 'transcript_name';
4833
+ }
4834
+ const { attributes } = feature;
4835
+ const name = attributes?.[attrName];
4836
+ if (name) {
4837
+ return name[0];
4838
+ }
4839
+ return getFeatureId(feature);
4981
4840
  };
4982
4841
  function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
4983
4842
  const apolloSessionModel = session;
@@ -5002,6 +4861,9 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5002
4861
  const getFeatures = (min, max) => {
5003
4862
  const filteredFeatures = [];
5004
4863
  for (const [, f] of features) {
4864
+ if (f.type === 'chromosome') {
4865
+ continue;
4866
+ }
5005
4867
  const featureSnapshot = getSnapshot(f);
5006
4868
  if (min >= featureSnapshot.min && max <= featureSnapshot.max) {
5007
4869
  filteredFeatures.push(featureSnapshot);
@@ -5011,27 +4873,27 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5011
4873
  };
5012
4874
  useEffect(() => {
5013
4875
  setErrorMessage('');
5014
- if (checkedChildrens.length === 0) {
5015
- setParentFeatureChecked(false);
5016
- return;
5017
- }
4876
+ let mins = [];
4877
+ let maxes = [];
5018
4878
  if (annotationFeature.children) {
5019
4879
  const checkedAnnotationFeatureChildren = Object.values(annotationFeature.children)
5020
4880
  .filter((child) => isTranscript(child, apolloSessionModel))
5021
4881
  .filter((child) => checkedChildrens.includes(child._id));
5022
- const mins = checkedAnnotationFeatureChildren.map((f) => f.min);
5023
- const maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
5024
- const min = Math.min(...mins);
5025
- const max = Math.max(...maxes);
5026
- const filteredFeatures = getFeatures(min, max);
5027
- setDestinationFeatures(filteredFeatures);
5028
- if (filteredFeatures.length === 0 &&
5029
- checkedChildrens.length > 0 &&
5030
- !parentFeatureChecked) {
5031
- setErrorMessage('No destination features found');
5032
- }
5033
- }
5034
- }, [checkedChildrens]);
4882
+ mins = checkedAnnotationFeatureChildren.map((f) => f.min);
4883
+ maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
4884
+ }
4885
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4886
+ if (featureTypeOntology &&
4887
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript')) {
4888
+ mins = [annotationFeature.min, ...mins];
4889
+ maxes = [annotationFeature.max, ...maxes];
4890
+ }
4891
+ const min = Math.min(...mins);
4892
+ const max = Math.max(...maxes);
4893
+ const filteredFeatures = getFeatures(min, max);
4894
+ setDestinationFeatures(filteredFeatures);
4895
+ setSelectedDestinationFeature(filteredFeatures[0]);
4896
+ }, [checkedChildrens, parentFeatureChecked]);
5035
4897
  const handleParentFeatureCheck = (event) => {
5036
4898
  const isChecked = event.target.checked;
5037
4899
  setParentFeatureChecked(isChecked);
@@ -5048,12 +4910,52 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5048
4910
  };
5049
4911
  const handleCreateApolloAnnotation = async () => {
5050
4912
  if (parentFeatureChecked) {
5051
- const change = new AddFeatureChange({
5052
- changedIds: [annotationFeature._id],
5053
- typeName: 'AddFeatureChange',
5054
- assembly: assembly.name,
5055
- addedFeature: annotationFeature,
5056
- });
4913
+ let change;
4914
+ if (isGene(annotationFeature, apolloSessionModel)) {
4915
+ if (annotationFeature.children &&
4916
+ checkedChildrens.length !==
4917
+ Object.values(annotationFeature.children).length) {
4918
+ const childrens = {};
4919
+ for (const childId of checkedChildrens) {
4920
+ childrens[childId] = annotationFeature.children[childId];
4921
+ }
4922
+ change = new AddFeatureChange({
4923
+ changedIds: [annotationFeature._id],
4924
+ typeName: 'AddFeatureChange',
4925
+ assembly: assembly.name,
4926
+ addedFeature: {
4927
+ ...annotationFeature,
4928
+ children: childrens,
4929
+ },
4930
+ });
4931
+ }
4932
+ else {
4933
+ change = new AddFeatureChange({
4934
+ changedIds: [annotationFeature._id],
4935
+ typeName: 'AddFeatureChange',
4936
+ assembly: assembly.name,
4937
+ addedFeature: annotationFeature,
4938
+ });
4939
+ }
4940
+ }
4941
+ if (isTranscript(annotationFeature, apolloSessionModel)) {
4942
+ if (selectedDestinationFeature) {
4943
+ change = new AddFeatureChange({
4944
+ parentFeatureId: selectedDestinationFeature._id,
4945
+ changedIds: [selectedDestinationFeature._id],
4946
+ typeName: 'AddFeatureChange',
4947
+ assembly: assembly.name,
4948
+ addedFeature: annotationFeature,
4949
+ });
4950
+ }
4951
+ else {
4952
+ setErrorMessage('There is no destination gene for this transcript');
4953
+ return;
4954
+ }
4955
+ }
4956
+ if (!change) {
4957
+ return;
4958
+ }
5057
4959
  await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5058
4960
  session.notify('Annotation added successfully', 'success');
5059
4961
  handleClose();
@@ -5075,27 +4977,28 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5075
4977
  addedFeature: child,
5076
4978
  });
5077
4979
  await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5078
- session.notify('Annotation added successfully', 'success');
5079
- handleClose();
5080
4980
  }
4981
+ session.notify('Annotation added successfully', 'success');
4982
+ handleClose();
5081
4983
  }
5082
4984
  };
5083
4985
  return (React__default.createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
5084
4986
  React__default.createElement(DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
5085
4987
  React__default.createElement(DialogContent, null,
5086
4988
  React__default.createElement(Box, { sx: { ml: 3 } },
5087
- isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default.createElement(FormControlLabel, { control: React__default.createElement(Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${annotationFeature.type}:${annotationFeature.min}..${annotationFeature.max}` })),
4989
+ isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default.createElement(FormControlLabel, { control: React__default.createElement(Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${getFeatureNameOrId(annotationFeature, apolloSessionModel)} (${annotationFeature.min + 1}..${annotationFeature.max})` })),
5088
4990
  annotationFeature.children && (React__default.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
5089
4991
  .filter((child) => isTranscript(child, apolloSessionModel))
5090
4992
  .map((child) => (React__default.createElement(FormControlLabel, { key: child._id, control: React__default.createElement(Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
5091
4993
  handleChildFeatureCheck(e, child);
5092
- } }), label: `${child.type}:${child.min}..${child.max}` })))))),
5093
- !parentFeatureChecked &&
5094
- checkedChildrens.length > 0 &&
5095
- destinationFeatures.length > 0 && (React__default.createElement(Box, { sx: { ml: 3 } },
4994
+ } }), label: `${getFeatureNameOrId(child, apolloSessionModel)} (${child.min + 1}..${child.max})` })))))),
4995
+ destinationFeatures.length > 0 &&
4996
+ ((!parentFeatureChecked && checkedChildrens.length > 0) ||
4997
+ (parentFeatureChecked &&
4998
+ isTranscript(annotationFeature, apolloSessionModel))) && (React__default.createElement(Box, { sx: { ml: 3 } },
5096
4999
  React__default.createElement(Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
5097
5000
  React__default.createElement(Box, { sx: { mt: 1 } },
5098
- React__default.createElement(Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default.createElement(MenuItem, { key: f._id, value: f._id }, `${f.type}:${f.min}..${f.max}`)))))))),
5001
+ React__default.createElement(Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default.createElement(MenuItem, { key: f._id, value: f._id }, `${getFeatureNameOrId(f, apolloSessionModel)} (${f.min}..${f.max})`)))))))),
5099
5002
  React__default.createElement(DialogActions, null,
5100
5003
  React__default.createElement(Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
5101
5004
  (!parentFeatureChecked &&
@@ -5107,6 +5010,7 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
5107
5010
  }
5108
5011
 
5109
5012
  function simpleFeatureToGFF3Feature(feature, refSeqId) {
5013
+ // eslint-disable-next-line unicorn/prefer-structured-clone
5110
5014
  const xfeature = JSON.parse(JSON.stringify(feature));
5111
5015
  const children = xfeature.subfeatures;
5112
5016
  const gff3Feature = [
@@ -5151,93 +5055,262 @@ function convertFeatureAttributes(feature) {
5151
5055
  if (defaultFields.has(key)) {
5152
5056
  continue;
5153
5057
  }
5154
- attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
5155
- }
5156
- return attributes;
5157
- }
5158
- function annotationFromJBrowseFeature(pluggableElement) {
5159
- if (pluggableElement.name !== 'LinearBasicDisplay') {
5160
- return pluggableElement;
5161
- }
5162
- const { stateModel } = pluggableElement;
5163
- const newStateModel = stateModel
5164
- .views((self) => ({
5165
- getFirstRegion() {
5166
- const lgv = getContainingView(self);
5167
- return lgv.dynamicBlocks.contentBlocks[0];
5168
- },
5169
- getAssembly() {
5170
- const firstRegion = self.getFirstRegion();
5171
- const session = getSession(self);
5172
- const { assemblyManager } = session;
5173
- const { assemblyName } = firstRegion;
5174
- const assembly = assemblyManager.get(assemblyName);
5175
- if (!assembly) {
5176
- throw new Error(`Could not find assembly named ${assemblyName}`);
5058
+ attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
5059
+ }
5060
+ return attributes;
5061
+ }
5062
+ function annotationFromJBrowseFeature(pluggableElement) {
5063
+ if (pluggableElement.name !== 'LinearBasicDisplay') {
5064
+ return pluggableElement;
5065
+ }
5066
+ const { stateModel } = pluggableElement;
5067
+ const newStateModel = stateModel
5068
+ .views((self) => ({
5069
+ getFirstRegion() {
5070
+ const lgv = getContainingView(self);
5071
+ return lgv.dynamicBlocks.contentBlocks[0];
5072
+ },
5073
+ getAssembly() {
5074
+ const firstRegion = self.getFirstRegion();
5075
+ const session = getSession(self);
5076
+ const { assemblyManager } = session;
5077
+ const { assemblyName } = firstRegion;
5078
+ const assembly = assemblyManager.get(assemblyName);
5079
+ if (!assembly) {
5080
+ throw new Error(`Could not find assembly named ${assemblyName}`);
5081
+ }
5082
+ return assembly;
5083
+ },
5084
+ getRefSeqId(assembly) {
5085
+ const firstRegion = self.getFirstRegion();
5086
+ const { refName } = firstRegion;
5087
+ const { refNameAliases } = assembly;
5088
+ if (!refNameAliases) {
5089
+ throw new Error(`Could not find aliases for ${assembly.name}`);
5090
+ }
5091
+ const newRefNames = [...Object.entries(refNameAliases)]
5092
+ .filter(([id, refName]) => id !== refName)
5093
+ .map(([id, refName]) => ({
5094
+ _id: id,
5095
+ name: refName,
5096
+ }));
5097
+ const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
5098
+ if (!refSeqId) {
5099
+ throw new Error(`Could not find refSeqId named ${refName}`);
5100
+ }
5101
+ return refSeqId;
5102
+ },
5103
+ getAnnotationFeature(assembly) {
5104
+ const refSeqId = self.getRefSeqId(assembly);
5105
+ const sfeature = self.contextMenuFeature.data;
5106
+ return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
5107
+ },
5108
+ }))
5109
+ .views((self) => {
5110
+ const superContextMenuItems = self.contextMenuItems;
5111
+ return {
5112
+ contextMenuItems() {
5113
+ const session = getSession(self);
5114
+ const assembly = self.getAssembly();
5115
+ const feature = self.contextMenuFeature;
5116
+ if (!feature) {
5117
+ return superContextMenuItems();
5118
+ }
5119
+ return [
5120
+ ...superContextMenuItems(),
5121
+ {
5122
+ label: 'Create Apollo annotation',
5123
+ icon: AddIcon,
5124
+ onClick: () => {
5125
+ session.queueDialog((doneCallback) => [
5126
+ CreateApolloAnnotation,
5127
+ {
5128
+ session,
5129
+ handleClose: () => {
5130
+ doneCallback();
5131
+ },
5132
+ annotationFeature: self.getAnnotationFeature(assembly),
5133
+ assembly,
5134
+ refSeqId: self.getRefSeqId(assembly),
5135
+ },
5136
+ ]);
5137
+ },
5138
+ },
5139
+ ];
5140
+ },
5141
+ };
5142
+ });
5143
+ pluggableElement.stateModel = newStateModel;
5144
+ return pluggableElement;
5145
+ }
5146
+
5147
+ /* eslint-disable @typescript-eslint/unbound-method */
5148
+ // interface TermAutocompleteResult extends TermValue {
5149
+ // label: string[]
5150
+ // match: string
5151
+ // category: string[]
5152
+ // taxon: string
5153
+ // taxon_label: string
5154
+ // highlight: string
5155
+ // has_highlight: boolean
5156
+ // }
5157
+ // interface TermAutocompleteResponse {
5158
+ // docs: TermAutocompleteResult[]
5159
+ // }
5160
+ // const hiliteRegex = /(?<=<em class="hilite">)(.*?)(?=<\/em>)/g
5161
+ function TermTagWithTooltip({ getTagProps, index, ontology, termId, }) {
5162
+ const manager = getParent(ontology, 2);
5163
+ const [description, setDescription] = React.useState('');
5164
+ const [errorMessage, setErrorMessage] = React.useState('');
5165
+ React.useEffect(() => {
5166
+ const controller = new AbortController();
5167
+ const { signal } = controller;
5168
+ async function fetchDescription() {
5169
+ const termUrl = manager.expandPrefixes(termId);
5170
+ const db = await ontology.dataStore?.db;
5171
+ if (!db || signal.aborted) {
5172
+ return;
5173
+ }
5174
+ const term = await db
5175
+ .transaction('nodes')
5176
+ .objectStore('nodes')
5177
+ .get(termUrl);
5178
+ if (term && term.lbl && !signal.aborted) {
5179
+ setDescription(term.lbl || 'no label');
5180
+ }
5181
+ }
5182
+ fetchDescription().catch((error) => {
5183
+ if (!signal.aborted) {
5184
+ setErrorMessage(String(error));
5185
+ }
5186
+ });
5187
+ return () => {
5188
+ controller.abort();
5189
+ };
5190
+ }, [termId, ontology, manager]);
5191
+ return (React.createElement(Tooltip, { title: description },
5192
+ React.createElement("div", null,
5193
+ React.createElement(Chip, { label: errorMessage || manager.applyPrefixes(termId), color: errorMessage ? 'error' : 'default', size: "small", ...getTagProps({ index }) }))));
5194
+ }
5195
+ function OntologyTermMultiSelect({ includeDeprecated, onChange, ontologyName, ontologyVersion, session, value: initialValue, label, }) {
5196
+ const { ontologyManager } = session.apolloDataStore;
5197
+ const ontology = ontologyManager.findOntology(ontologyName, ontologyVersion);
5198
+ const [value, setValue] = React.useState(initialValue.map((id) => ({ term: { id, type: 'CLASS' } })));
5199
+ const [inputValue, setInputValue] = React.useState('');
5200
+ const [options, setOptions] = React.useState([]);
5201
+ const [loading, setLoading] = React.useState(false);
5202
+ const [errorMessage, setErrorMessage] = React.useState('');
5203
+ const getOntologyTerms = React.useMemo(() => debounce(async (request, callback) => {
5204
+ if (!ontology) {
5205
+ return;
5206
+ }
5207
+ const { dataStore } = ontology;
5208
+ if (!dataStore) {
5209
+ return;
5210
+ }
5211
+ const { input, signal } = request;
5212
+ try {
5213
+ const matches = await dataStore.getTermsByFulltext(input, undefined, signal);
5214
+ // aggregate the matches by term
5215
+ const byTerm = new Map();
5216
+ const options = [];
5217
+ for (const match of matches) {
5218
+ if (!isOntologyClass(match.term) ||
5219
+ (!includeDeprecated && isDeprecated(match.term))) {
5220
+ continue;
5221
+ }
5222
+ let slot = byTerm.get(match.term.id);
5223
+ if (!slot) {
5224
+ slot = { term: match.term, matches: [] };
5225
+ byTerm.set(match.term.id, slot);
5226
+ options.push(slot);
5227
+ }
5228
+ slot.matches.push(match);
5229
+ }
5230
+ callback(options);
5231
+ }
5232
+ catch (error) {
5233
+ if (!isAbortException(error)) {
5234
+ setErrorMessage(String(error));
5177
5235
  }
5178
- return assembly;
5179
- },
5180
- getRefSeqId(assembly) {
5181
- const firstRegion = self.getFirstRegion();
5182
- const { refName } = firstRegion;
5183
- const { refNameAliases } = assembly;
5184
- if (!refNameAliases) {
5185
- throw new Error(`Could not find aliases for ${assembly.name}`);
5236
+ }
5237
+ }, 400), [includeDeprecated, ontology]);
5238
+ React.useEffect(() => {
5239
+ const aborter = new AbortController();
5240
+ const { signal } = aborter;
5241
+ if (inputValue === '') {
5242
+ setOptions([]);
5243
+ return;
5244
+ }
5245
+ setLoading(true);
5246
+ void getOntologyTerms({ input: inputValue, signal }, (results) => {
5247
+ let newOptions = [];
5248
+ if (value.length > 0) {
5249
+ newOptions = value;
5186
5250
  }
5187
- const newRefNames = [...Object.entries(refNameAliases)]
5188
- .filter(([id, refName]) => id !== refName)
5189
- .map(([id, refName]) => ({
5190
- _id: id,
5191
- name: refName,
5192
- }));
5193
- const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
5194
- if (!refSeqId) {
5195
- throw new Error(`Could not find refSeqId named ${refName}`);
5251
+ if (results) {
5252
+ newOptions = [...newOptions, ...results];
5196
5253
  }
5197
- return refSeqId;
5198
- },
5199
- getAnnotationFeature(assembly) {
5200
- const refSeqId = self.getRefSeqId(assembly);
5201
- const sfeature = self.contextMenuFeature.data;
5202
- return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
5203
- },
5204
- }))
5205
- .views((self) => {
5206
- const superContextMenuItems = self.contextMenuItems;
5207
- const session = getSession(self);
5208
- const assembly = self.getAssembly();
5209
- return {
5210
- contextMenuItems() {
5211
- const feature = self.contextMenuFeature;
5212
- if (!feature) {
5213
- return superContextMenuItems();
5214
- }
5215
- return [
5216
- ...superContextMenuItems(),
5217
- {
5218
- label: 'Create Apollo annotation',
5219
- icon: AddIcon,
5220
- onClick: () => {
5221
- session.queueDialog((doneCallback) => [
5222
- CreateApolloAnnotation,
5223
- {
5224
- session,
5225
- handleClose: () => {
5226
- doneCallback();
5227
- },
5228
- annotationFeature: self.getAnnotationFeature(assembly),
5229
- assembly,
5230
- refSeqId: self.getRefSeqId(assembly),
5231
- },
5232
- ]);
5233
- },
5234
- },
5235
- ];
5236
- },
5254
+ setOptions(newOptions);
5255
+ setLoading(false);
5256
+ });
5257
+ return () => {
5258
+ aborter.abort();
5237
5259
  };
5260
+ }, [getOntologyTerms, ontology, includeDeprecated, inputValue, value]);
5261
+ if (!ontology) {
5262
+ return null;
5263
+ }
5264
+ const extraTextFieldParams = {};
5265
+ if (errorMessage) {
5266
+ extraTextFieldParams.error = true;
5267
+ extraTextFieldParams.helperText = errorMessage;
5268
+ }
5269
+ return (React.createElement(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) ===
5270
+ ontologyManager.applyPrefixes(v.term.id), noOptionsText: inputValue ? 'No matches' : 'Start typing to search', onChange: (_, newValue) => {
5271
+ setOptions(newValue ? [...newValue, ...options] : options);
5272
+ onChange(newValue.map((v) => ontologyManager.applyPrefixes(v.term.id)));
5273
+ setValue(newValue);
5274
+ }, onInputChange: (event, newInputValue) => {
5275
+ if (newInputValue) {
5276
+ setLoading(true);
5277
+ }
5278
+ setOptions([]);
5279
+ setInputValue(newInputValue);
5280
+ }, multiple: true, renderInput: (params) => (React.createElement(TextField, { ...params, ...extraTextFieldParams, variant: "outlined", label: label, fullWidth: true })), renderOption: (props, option) => (React.createElement(Option, { ...props, ontologyManager: ontologyManager, option: option, inputValue: inputValue })), renderTags: (v, getTagProps) => v.map((option, index) => (React.createElement(TermTagWithTooltip, { termId: option.term.id, index: index, ontology: ontology, getTagProps: getTagProps, key: option.term.id }))) }));
5281
+ }
5282
+ function HighlightedText(props) {
5283
+ const { search, str } = props;
5284
+ const highlights = highlightMatch(str, search, {
5285
+ insideWords: true,
5286
+ findAllOccurrences: true,
5238
5287
  });
5239
- pluggableElement.stateModel = newStateModel;
5240
- return pluggableElement;
5288
+ const parts = highlightParse(str, highlights);
5289
+ return (React.createElement(React.Fragment, null, parts.map((part, index) => (React.createElement(Typography, { key: index, component: "span", sx: { fontWeight: part.highlight ? 'bold' : 'regular' }, variant: "body2", color: "text.secondary" }, part.text)))));
5290
+ }
5291
+ function Option(props) {
5292
+ const { inputValue, ontologyManager, option, ...other } = props;
5293
+ const matches = option.matches ?? [];
5294
+ const fields = matches
5295
+ .filter((match) => match.field.jsonPath !== '$.lbl')
5296
+ .map((match) => {
5297
+ return (React.createElement(React.Fragment, { key: `option-${match.term.id}-${match.str}` },
5298
+ React.createElement(Typography, { component: "dt", variant: "body2", color: "text.secondary" }, match.field.displayName),
5299
+ React.createElement("dd", null,
5300
+ React.createElement(HighlightedText, { str: match.str, search: inputValue }))));
5301
+ });
5302
+ // const lblScore = matches
5303
+ // .filter((match) => match.field.jsonPath === '$.lbl')
5304
+ // .map((m) => m.score)
5305
+ // .join(', ')
5306
+ return (React.createElement("li", { ...other },
5307
+ React.createElement(Grid2, { container: true },
5308
+ React.createElement(Grid2, null,
5309
+ React.createElement(Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
5310
+ ' ',
5311
+ React.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
5312
+ ' ',
5313
+ React.createElement("dl", null, fields)))));
5241
5314
  }
5242
5315
 
5243
5316
  /* eslint-disable @typescript-eslint/unbound-method */
@@ -5278,13 +5351,13 @@ const reservedKeys = new Map([
5278
5351
  [
5279
5352
  'Gene Ontology',
5280
5353
  (props) => {
5281
- return React__default.createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Gene Ontology" });
5354
+ return (React__default.createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Gene Ontology", label: 'Gene Ontology' }));
5282
5355
  },
5283
5356
  ],
5284
5357
  [
5285
5358
  'Sequence Ontology',
5286
5359
  (props) => {
5287
- return (React__default.createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Sequence Ontology" }));
5360
+ return (React__default.createElement(OntologyTermMultiSelect, { ...props, ontologyName: "Sequence Ontology", label: 'Sequence Ontology' }));
5288
5361
  },
5289
5362
  ],
5290
5363
  ]);
@@ -5311,17 +5384,13 @@ const useStyles$a = makeStyles()((theme) => ({
5311
5384
  },
5312
5385
  }));
5313
5386
  function CustomAttributeValueEditor(props) {
5314
- const { onChange, value } = props;
5387
+ const { onChange, value, label } = props;
5315
5388
  return (React__default.createElement(StringTextField, { value: value, onChangeCommitted: (newValue) => {
5316
5389
  onChange(newValue.split(','));
5317
- }, variant: "outlined", fullWidth: true, helperText: "Separate multiple values for the attribute with commas" }));
5390
+ }, variant: "outlined", fullWidth: true, label: label, style: { width: '100%' } }));
5318
5391
  }
5319
- const Attributes = observer(function Attributes({ assembly, editable, feature, session, }) {
5320
- const [errorMessage, setErrorMessage] = useState('');
5321
- const [showAddNewForm, setShowAddNewForm] = useState(false);
5322
- const { classes } = useStyles$a();
5323
- const [newAttributeKey, setNewAttributeKey] = useState('');
5324
- const attributes = Object.fromEntries([...feature.attributes.entries()].map(([key, value]) => {
5392
+ function transformAttributes(feature) {
5393
+ return Object.fromEntries([...feature.attributes.entries()].map(([key, value]) => {
5325
5394
  if (key.startsWith('gff_')) {
5326
5395
  const newKey = key.slice(4);
5327
5396
  const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1);
@@ -5332,17 +5401,23 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
5332
5401
  }
5333
5402
  return [key, getSnapshot(value)];
5334
5403
  }));
5404
+ }
5405
+ const Attributes = observer(function Attributes({ assembly, editable, feature, session, }) {
5406
+ const [errorMessage, setErrorMessage] = useState('');
5407
+ const [showAddNewForm, setShowAddNewForm] = useState(false);
5408
+ const { classes } = useStyles$a();
5409
+ const [newAttributeKey, setNewAttributeKey] = useState('');
5410
+ const [attributes, setAttributes] = useState(() => transformAttributes(feature));
5411
+ useEffect(() => {
5412
+ setAttributes(transformAttributes(feature));
5413
+ }, [feature]);
5335
5414
  const { notify } = session;
5336
5415
  const { changeManager } = session.apolloDataStore;
5337
- async function onChangeCommitted(newKey, newValue) {
5416
+ async function onChangeCommitted(attributes) {
5338
5417
  setErrorMessage('');
5339
5418
  const attrs = {};
5340
5419
  if (attributes) {
5341
- const modifiedAttrs = Object.entries({
5342
- ...attributes,
5343
- [newKey]: newValue,
5344
- });
5345
- for (const [key, val] of modifiedAttrs) {
5420
+ for (const [key, val] of Object.entries(attributes)) {
5346
5421
  if (!val) {
5347
5422
  continue;
5348
5423
  }
@@ -5431,7 +5506,25 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
5431
5506
  setErrorMessage(`Key cannot starts with uppercase letter unless key is one of these: ${reservedTerms.join(', ')}`);
5432
5507
  return;
5433
5508
  }
5434
- void onChangeCommitted(newAttributeKey, []);
5509
+ setAttributes({
5510
+ ...attributes,
5511
+ [newAttributeKey]: [],
5512
+ });
5513
+ setShowAddNewForm(false);
5514
+ setNewAttributeKey('');
5515
+ }
5516
+ function deleteAttribute(key) {
5517
+ const newAttributes = { ...attributes };
5518
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
5519
+ delete newAttributes[key];
5520
+ setAttributes(newAttributes);
5521
+ void onChangeCommitted(newAttributes);
5522
+ }
5523
+ function updateAttribute(key, newValue) {
5524
+ const newAttributes = { ...attributes };
5525
+ newAttributes[key] = newValue;
5526
+ setAttributes(newAttributes);
5527
+ void onChangeCommitted(newAttributes);
5435
5528
  }
5436
5529
  function handleRadioButtonChange(event, value) {
5437
5530
  if (value === 'custom') {
@@ -5444,22 +5537,22 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
5444
5537
  setErrorMessage('Unknown attribute type');
5445
5538
  }
5446
5539
  }
5447
- return (React__default.createElement(React__default.Fragment, null,
5448
- React__default.createElement(Typography, { variant: "h5" }, "Attributes"),
5540
+ return (React__default.createElement("div", { "data-testid": "attributes_test" },
5449
5541
  React__default.createElement(Grid2, { container: true, direction: "column", spacing: 1 },
5450
5542
  Object.entries(attributes).map(([key, value]) => {
5451
5543
  if (key === '') {
5452
5544
  return null;
5453
5545
  }
5454
5546
  const EditorComponent = reservedKeys.get(key) ?? CustomAttributeValueEditor;
5455
- return (React__default.createElement(Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
5456
- React__default.createElement(Grid2, null,
5457
- React__default.createElement(Paper, { variant: "outlined", className: classes.attributeName },
5458
- React__default.createElement(Typography, null, key))),
5459
- React__default.createElement(Grid2, { flexGrow: 1 },
5460
- React__default.createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => onChangeCommitted(key, newValue) })),
5461
- React__default.createElement(Grid2, null,
5462
- React__default.createElement(IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => onChangeCommitted(key) },
5547
+ return (React__default.createElement(Grid2, { container: true, key: key },
5548
+ React__default.createElement(Grid2, { size: 11 },
5549
+ React__default.createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => {
5550
+ updateAttribute(key, newValue);
5551
+ }, label: key })),
5552
+ React__default.createElement(Grid2, { size: 1 },
5553
+ React__default.createElement(IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
5554
+ deleteAttribute(key);
5555
+ }, style: { marginTop: '10px' } },
5463
5556
  React__default.createElement(DeleteIcon, { fontSize: "medium", key: key })))));
5464
5557
  }),
5465
5558
  React__default.createElement(Grid2, null,
@@ -5600,8 +5693,7 @@ const BasicInformation = observer(function BasicInformation({ assembly, feature,
5600
5693
  }
5601
5694
  return terms;
5602
5695
  }
5603
- return (React__default.createElement(React__default.Fragment, null,
5604
- React__default.createElement(Typography, { variant: "h5" }, "Basic information"),
5696
+ return (React__default.createElement("div", { "data-testid": "basic_information" },
5605
5697
  React__default.createElement(NumberTextField, { margin: "dense", id: "start", label: "Start", fullWidth: true, variant: "outlined", value: min + 1, onChangeCommitted: handleStartChange }),
5606
5698
  React__default.createElement(NumberTextField, { margin: "dense", id: "end", label: "End", fullWidth: true, variant: "outlined", value: max, onChangeCommitted: handleEndChange }),
5607
5699
  React__default.createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTerms.bind(null, feature), renderInput: (params) => (React__default.createElement(TextField, { ...params, label: "Type", variant: "outlined", fullWidth: true, error: Boolean(typeWarningText), helperText: typeWarningText })), onChange: (oldValue, newValue) => {
@@ -5634,11 +5726,7 @@ const useStyles$9 = makeStyles()({
5634
5726
  });
5635
5727
  const Sequence = observer(function Sequence({ assembly, feature, refName, session, }) {
5636
5728
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5637
- const [showSequence, setShowSequence] = useState(false);
5638
5729
  const { classes } = useStyles$9();
5639
- const onButtonClick = () => {
5640
- setShowSequence(!showSequence);
5641
- };
5642
5730
  if (!(feature && currentAssembly)) {
5643
5731
  return null;
5644
5732
  }
@@ -5647,22 +5735,17 @@ const Sequence = observer(function Sequence({ assembly, feature, refName, sessio
5647
5735
  return null;
5648
5736
  }
5649
5737
  const { max, min } = feature;
5650
- let sequence = '';
5651
- if (showSequence) {
5652
- sequence = refSeq.getSequence(min, max);
5653
- if (sequence) {
5654
- sequence = formatSequence(sequence, refName, min, max);
5655
- }
5656
- else {
5657
- void session.apolloDataStore.loadRefSeq([
5658
- { assemblyName: assembly, refName, start: min, end: max },
5659
- ]);
5660
- }
5738
+ let sequence = refSeq.getSequence(min, max);
5739
+ if (sequence) {
5740
+ sequence = formatSequence(sequence, refName, min, max);
5661
5741
  }
5662
- return (React__default.createElement(React__default.Fragment, null,
5663
- React__default.createElement(Typography, { variant: "h5" }, "Sequence"),
5664
- React__default.createElement(Button, { variant: "contained", onClick: onButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence'),
5665
- React__default.createElement("div", null, showSequence && (React__default.createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })))));
5742
+ else {
5743
+ void session.apolloDataStore.loadRefSeq([
5744
+ { assemblyName: assembly, refName, start: min, end: max },
5745
+ ]);
5746
+ }
5747
+ return (React__default.createElement("div", null,
5748
+ React__default.createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })));
5666
5749
  });
5667
5750
 
5668
5751
  const FeatureDetailsNavigation = observer(function FeatureDetailsNavigation(props) {
@@ -5677,14 +5760,14 @@ const FeatureDetailsNavigation = observer(function FeatureDetailsNavigation(prop
5677
5760
  if (!(parent ?? childFeatures.length > 0)) {
5678
5761
  return null;
5679
5762
  }
5680
- return (React__default.createElement("div", null,
5681
- React__default.createElement(Typography, { variant: "h5" }, "Go to related feature"),
5763
+ return (React__default.createElement("div", { style: { marginTop: 10 } },
5682
5764
  parent && (React__default.createElement("div", null,
5683
5765
  React__default.createElement(Typography, { variant: "h6" }, "Parent:"),
5684
5766
  React__default.createElement(Button, { variant: "contained", onClick: () => {
5685
5767
  model.setFeature(parent);
5686
5768
  } },
5687
5769
  parent.type,
5770
+ getFeatureNameOrId$1(parent),
5688
5771
  " (",
5689
5772
  parent.min,
5690
5773
  "..",
@@ -5699,6 +5782,7 @@ const FeatureDetailsNavigation = observer(function FeatureDetailsNavigation(prop
5699
5782
  model.setFeature(child);
5700
5783
  } },
5701
5784
  child.type,
5785
+ getFeatureNameOrId$1(child),
5702
5786
  " (",
5703
5787
  child.min,
5704
5788
  "..",
@@ -5717,6 +5801,10 @@ const ApolloFeatureDetailsWidget = observer(function ApolloFeatureDetailsWidget(
5717
5801
  const session = getSession(model);
5718
5802
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5719
5803
  const { classes } = useStyles$8();
5804
+ const [panelState, setPanelState] = useState(['attributes']);
5805
+ useEffect(() => {
5806
+ setPanelState(['attributes']);
5807
+ }, [feature]);
5720
5808
  if (!(feature && currentAssembly)) {
5721
5809
  return null;
5722
5810
  }
@@ -5731,14 +5819,36 @@ const ApolloFeatureDetailsWidget = observer(function ApolloFeatureDetailsWidget(
5731
5819
  { assemblyName: assembly, refName, start: min, end: max },
5732
5820
  ]);
5733
5821
  }
5822
+ function handlePanelChange(expanded, panel) {
5823
+ if (expanded) {
5824
+ setPanelState([...panelState, panel]);
5825
+ }
5826
+ else {
5827
+ setPanelState(panelState.filter((p) => p !== panel));
5828
+ }
5829
+ }
5734
5830
  return (React__default.createElement("div", { className: classes.root },
5735
5831
  React__default.createElement(BasicInformation, { feature: feature, session: session, assembly: currentAssembly._id }),
5736
- React__default.createElement("hr", null),
5737
- React__default.createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }),
5738
- React__default.createElement("hr", null),
5739
- React__default.createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName }),
5740
- React__default.createElement("hr", null),
5741
- React__default.createElement(FeatureDetailsNavigation, { model: model, feature: feature })));
5832
+ React__default.createElement(Accordion, { style: { marginTop: 10 }, expanded: panelState.includes('attributes'), onChange: (e, expanded) => {
5833
+ handlePanelChange(expanded, 'attributes');
5834
+ } },
5835
+ React__default.createElement(AccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
5836
+ React__default.createElement(Typography, { component: "span" }, "Attributes")),
5837
+ React__default.createElement(AccordionDetails, null,
5838
+ React__default.createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }))),
5839
+ React__default.createElement(Accordion, { style: { marginTop: 10 }, expanded: panelState.includes('sequence'), onChange: (e, expanded) => {
5840
+ handlePanelChange(expanded, 'sequence');
5841
+ } },
5842
+ React__default.createElement(AccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel2-content", id: "panel2-header" },
5843
+ React__default.createElement(Typography, { component: "span" }, "Sequence")),
5844
+ React__default.createElement(AccordionDetails, null, panelState.includes('sequence') && (React__default.createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName })))),
5845
+ React__default.createElement(Accordion, { style: { marginTop: 10 }, expanded: panelState.includes('related_features'), onChange: (e, expanded) => {
5846
+ handlePanelChange(expanded, 'related_features');
5847
+ } },
5848
+ React__default.createElement(AccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel3-content", id: "panel3-header" },
5849
+ React__default.createElement(Typography, { component: "span" }, "Related features")),
5850
+ React__default.createElement(AccordionDetails, null,
5851
+ React__default.createElement(FeatureDetailsNavigation, { model: model, feature: feature })))));
5742
5852
  });
5743
5853
 
5744
5854
  /* eslint-disable @typescript-eslint/no-unsafe-call */
@@ -5806,154 +5916,32 @@ const ApolloTranscriptDetailsModel = types
5806
5916
  .actions((self) => ({
5807
5917
  setFeature(feature) {
5808
5918
  // @ts-expect-error Not sure why TS thinks these MST types don't match
5809
- self.feature = feature;
5810
- },
5811
- setTryReload(featureId) {
5812
- self.tryReload = featureId;
5813
- },
5814
- }))
5815
- .actions((self) => ({
5816
- afterAttach() {
5817
- addDisposer(self, autorun((reaction) => {
5818
- if (!self.tryReload) {
5819
- return;
5820
- }
5821
- const session = getSession(self);
5822
- const { apolloDataStore } = session;
5823
- if (!apolloDataStore) {
5824
- return;
5825
- }
5826
- const feature = apolloDataStore.getFeature(self.tryReload);
5827
- if (feature) {
5828
- self.setFeature(feature);
5829
- self.setTryReload();
5830
- reaction.dispose();
5831
- }
5832
- }));
5833
- },
5834
- }));
5835
-
5836
- const TranscriptBasicInformation = observer(function TranscriptBasicInformation({ assembly, feature, refName, session, }) {
5837
- const { notify } = session;
5838
- const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5839
- const refData = currentAssembly?.getByRefName(refName);
5840
- const { changeManager } = session.apolloDataStore;
5841
- const theme = useTheme();
5842
- function handleLocationChange(oldLocation, newLocation, feature, isMin) {
5843
- if (!feature.children) {
5844
- throw new Error('Transcript should have child features');
5845
- }
5846
- for (const [, child] of feature.children) {
5847
- if (isMin && oldLocation - 1 === child.min) {
5848
- const change = new LocationStartChange({
5849
- typeName: 'LocationStartChange',
5850
- changedIds: [child._id],
5851
- featureId: feature._id,
5852
- oldStart: oldLocation - 1,
5853
- newStart: newLocation - 1,
5854
- assembly,
5855
- });
5856
- changeManager.submit(change).catch(() => {
5857
- notify('Error updating feature start position', 'error');
5858
- });
5859
- return;
5860
- }
5861
- if (!isMin && newLocation === child.max) {
5862
- const change = new LocationEndChange({
5863
- typeName: 'LocationEndChange',
5864
- changedIds: [child._id],
5865
- featureId: feature._id,
5866
- oldEnd: child.max,
5867
- newEnd: newLocation,
5868
- assembly,
5869
- });
5870
- changeManager.submit(change).catch(() => {
5871
- notify('Error updating feature start position', 'error');
5872
- });
5919
+ self.feature = feature;
5920
+ },
5921
+ setTryReload(featureId) {
5922
+ self.tryReload = featureId;
5923
+ },
5924
+ }))
5925
+ .actions((self) => ({
5926
+ afterAttach() {
5927
+ addDisposer(self, autorun((reaction) => {
5928
+ if (!self.tryReload) {
5873
5929
  return;
5874
5930
  }
5875
- }
5876
- }
5877
- if (!refData) {
5878
- return null;
5879
- }
5880
- let strand, transcriptParts;
5881
- try {
5882
- ;
5883
- ({ strand, transcriptParts } = feature);
5884
- }
5885
- catch {
5886
- return null;
5887
- }
5888
- const [firstLocation] = transcriptParts;
5889
- const locationData = firstLocation
5890
- .map((loc, idx) => {
5891
- const { max, min, type } = loc;
5892
- let label = type;
5893
- if (label === 'threePrimeUTR') {
5894
- label = '3` UTR';
5895
- }
5896
- else if (label === 'fivePrimeUTR') {
5897
- label = '5` UTR';
5898
- }
5899
- let fivePrimeSpliceSite;
5900
- let threePrimeSpliceSite;
5901
- let frameColor;
5902
- if (type === 'CDS') {
5903
- const { phase } = loc;
5904
- const frame = getFrame(min, max, strand ?? 1, phase);
5905
- frameColor = theme.palette.framesCDS.at(frame)?.main;
5906
- const previousLoc = firstLocation.at(idx - 1);
5907
- const nextLoc = firstLocation.at(idx + 1);
5908
- if (strand === 1) {
5909
- if (previousLoc?.type === 'intron') {
5910
- fivePrimeSpliceSite = refData.getSequence(min - 2, min);
5911
- }
5912
- if (nextLoc?.type === 'intron') {
5913
- threePrimeSpliceSite = refData.getSequence(max, max + 2);
5914
- }
5931
+ const session = getSession(self);
5932
+ const { apolloDataStore } = session;
5933
+ if (!apolloDataStore) {
5934
+ return;
5915
5935
  }
5916
- else {
5917
- if (previousLoc?.type === 'intron') {
5918
- fivePrimeSpliceSite = revcom(refData.getSequence(max, max + 2));
5919
- }
5920
- if (nextLoc?.type === 'intron') {
5921
- threePrimeSpliceSite = revcom(refData.getSequence(min - 2, min));
5922
- }
5936
+ const feature = apolloDataStore.getFeature(self.tryReload);
5937
+ if (feature) {
5938
+ self.setFeature(feature);
5939
+ self.setTryReload();
5940
+ reaction.dispose();
5923
5941
  }
5924
- }
5925
- return {
5926
- min,
5927
- max,
5928
- label,
5929
- fivePrimeSpliceSite,
5930
- threePrimeSpliceSite,
5931
- frameColor,
5932
- };
5933
- })
5934
- .filter((loc) => loc.label !== 'intron');
5935
- return (React__default.createElement(React__default.Fragment, null,
5936
- React__default.createElement(Typography, { variant: "h5" }, "Structure"),
5937
- React__default.createElement(Typography, { variant: "h6" },
5938
- strand === 1 ? 'Forward' : 'Reverse',
5939
- " strand"),
5940
- React__default.createElement(TableContainer, { component: Paper },
5941
- React__default.createElement(Table, { size: "small" },
5942
- React__default.createElement(TableBody, null, locationData.map((loc) => (React__default.createElement(TableRow, { key: `${loc.label}:${loc.min}-${loc.max}` },
5943
- React__default.createElement(TableCell, { component: "th", scope: "row", style: { background: loc.frameColor } }, loc.label),
5944
- React__default.createElement(TableCell, null, loc.fivePrimeSpliceSite ?? ''),
5945
- React__default.createElement(TableCell, { padding: "none" },
5946
- React__default.createElement(NumberTextField, { margin: "dense", variant: "outlined", value: strand === 1 ? loc.min + 1 : loc.max, onChangeCommitted: (newLocation) => {
5947
- handleLocationChange(strand === 1 ? loc.min + 1 : loc.max, newLocation, feature, strand === 1);
5948
- } })),
5949
- React__default.createElement(TableCell, { padding: "none" },
5950
- React__default.createElement(NumberTextField, { margin: "dense",
5951
- // disabled={item.type !== 'CDS'}
5952
- variant: "outlined", value: strand === 1 ? loc.max : loc.min + 1, onChangeCommitted: (newLocation) => {
5953
- handleLocationChange(strand === 1 ? loc.max : loc.min + 1, newLocation, feature, strand !== 1);
5954
- } })),
5955
- React__default.createElement(TableCell, null, loc.threePrimeSpliceSite ?? '')))))))));
5956
- });
5942
+ }));
5943
+ },
5944
+ }));
5957
5945
 
5958
5946
  const SEQUENCE_WRAP_LENGTH = 60;
5959
5947
  function getSequenceSegments(segmentType, feature, getSequence) {
@@ -6054,6 +6042,7 @@ function getSegmentColor(type) {
6054
6042
  case 'upOrDownstream': {
6055
6043
  return 'rgb(255,255,255)';
6056
6044
  }
6045
+ case 'exon':
6057
6046
  case 'UTR': {
6058
6047
  return 'rgb(194,106,119)';
6059
6048
  }
@@ -6068,13 +6057,54 @@ function getSegmentColor(type) {
6068
6057
  }
6069
6058
  }
6070
6059
  }
6060
+ function getLocationIntervals(seqSegments) {
6061
+ const locIntervals = [];
6062
+ const allLocs = seqSegments.flatMap((segment) => segment.locs);
6063
+ let [previous] = allLocs;
6064
+ for (let i = 1; i < allLocs.length; i++) {
6065
+ if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
6066
+ previous = {
6067
+ min: Math.min(previous.min, allLocs[i].min),
6068
+ max: Math.max(previous.max, allLocs[i].max),
6069
+ };
6070
+ }
6071
+ else {
6072
+ locIntervals.push(previous);
6073
+ previous = allLocs[i];
6074
+ }
6075
+ }
6076
+ locIntervals.push(previous);
6077
+ return locIntervals;
6078
+ }
6071
6079
  const TranscriptSequence = observer(function TranscriptSequence({ assembly, feature, refName, session, }) {
6072
6080
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
6073
6081
  const refData = currentAssembly?.getByRefName(refName);
6074
- const [showSequence, setShowSequence] = useState(false);
6075
- const [selectedOption, setSelectedOption] = useState('CDS');
6082
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
6083
+ const defaultSelectedOption = 'genomic';
6084
+ const defaultSequenceOptions = ['genomic', 'cDNA'];
6085
+ const [sequenceOptions, setSequenceOptions] = useState(defaultSequenceOptions);
6086
+ const [selectedOption, setSelectedOption] = useState(defaultSelectedOption);
6087
+ const [sequenceSegments, setSequenceSegments] = useState(() => {
6088
+ return refData
6089
+ ? getSequenceSegments(defaultSelectedOption, feature, (min, max) => refData.getSequence(min, max))
6090
+ : [];
6091
+ });
6092
+ const [locationIntervals, setLocationIntervals] = useState(() => {
6093
+ return getLocationIntervals(sequenceSegments);
6094
+ });
6076
6095
  const theme = useTheme();
6077
6096
  const seqRef = useRef(null);
6097
+ useEffect(() => {
6098
+ const { cdsLocations } = feature;
6099
+ const [firstLocation] = cdsLocations;
6100
+ if (firstLocation.length > 0) {
6101
+ setSequenceOptions([...defaultSequenceOptions, 'CDS', 'protein']);
6102
+ }
6103
+ else {
6104
+ setSequenceOptions(defaultSequenceOptions);
6105
+ }
6106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6107
+ }, [feature]);
6078
6108
  if (!(currentAssembly && refData)) {
6079
6109
  return null;
6080
6110
  }
@@ -6082,15 +6112,21 @@ const TranscriptSequence = observer(function TranscriptSequence({ assembly, feat
6082
6112
  if (!refSeq) {
6083
6113
  return null;
6084
6114
  }
6085
- if (feature.type !== 'mRNA') {
6115
+ if (!featureTypeOntology) {
6116
+ throw new Error('featureTypeOntology is undefined');
6117
+ }
6118
+ if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
6086
6119
  return null;
6087
6120
  }
6088
- const handleSeqButtonClick = () => {
6089
- setShowSequence(!showSequence);
6090
- };
6091
6121
  function handleChangeSeqOption(e) {
6092
6122
  const option = e.target.value;
6093
6123
  setSelectedOption(option);
6124
+ const seqSegments = refData
6125
+ ? getSequenceSegments(option, feature, (min, max) => refData.getSequence(min, max))
6126
+ : [];
6127
+ const locIntervals = getLocationIntervals(seqSegments);
6128
+ setSequenceSegments(seqSegments);
6129
+ setLocationIntervals(locIntervals);
6094
6130
  }
6095
6131
  // Function to copy text to clipboard
6096
6132
  const copyToClipboard = () => {
@@ -6106,62 +6142,419 @@ const TranscriptSequence = observer(function TranscriptSequence({ assembly, feat
6106
6142
  });
6107
6143
  void navigator.clipboard.write([clipboardItem]);
6108
6144
  };
6109
- const sequenceSegments = showSequence
6110
- ? getSequenceSegments(selectedOption, feature, (min, max) => refData.getSequence(min, max))
6111
- : [];
6112
- const locationIntervals = [];
6113
- if (showSequence) {
6114
- const allLocs = sequenceSegments.flatMap((segment) => segment.locs);
6115
- let [previous] = allLocs;
6116
- for (let i = 1; i < allLocs.length; i++) {
6117
- if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
6118
- previous = {
6119
- min: Math.min(previous.min, allLocs[i].min),
6120
- max: Math.max(previous.max, allLocs[i].max),
6121
- };
6145
+ return (React__default.createElement(React__default.Fragment, null,
6146
+ React__default.createElement(Select, { defaultValue: "genomic", value: selectedOption, onChange: handleChangeSeqOption, size: "small" }, sequenceOptions.map((option) => (React__default.createElement(MenuItem, { key: option, value: option }, option)))),
6147
+ React__default.createElement(Button, { variant: "contained", onClick: copyToClipboard, style: { marginLeft: 10 }, size: "medium" }, "Copy sequence"),
6148
+ React__default.createElement(Paper, { style: {
6149
+ fontFamily: 'monospace',
6150
+ padding: theme.spacing(),
6151
+ overflowX: 'auto',
6152
+ }, ref: seqRef },
6153
+ ">",
6154
+ refSeq.name,
6155
+ ":",
6156
+ locationIntervals
6157
+ .map((interval) => feature.strand === 1
6158
+ ? `${interval.min + 1}-${interval.max}`
6159
+ : `${interval.max}-${interval.min + 1}`)
6160
+ .join(';'),
6161
+ "(",
6162
+ feature.strand === 1 ? '+' : '-',
6163
+ ")",
6164
+ React__default.createElement("br", null),
6165
+ sequenceSegments.map((segment, index) => (React__default.createElement("span", { key: `${segment.type}-${index}`, style: {
6166
+ background: getSegmentColor(segment.type),
6167
+ color: theme.palette.getContrastText(getSegmentColor(segment.type)),
6168
+ } }, segment.sequenceLines.map((sequenceLine, idx) => (React__default.createElement(React__default.Fragment, { key: `${sequenceLine.slice(0, 5)}-${idx}` },
6169
+ sequenceLine,
6170
+ idx === segment.sequenceLines.length - 1 &&
6171
+ sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (React__default.createElement("br", null)))))))))));
6172
+ });
6173
+
6174
+ const HeaderTableCell = styled(TableCell)(() => ({
6175
+ fontWeight: 'bold',
6176
+ }));
6177
+ const TranscriptWidgetSummary = observer(function TranscriptWidgetSummary(props) {
6178
+ const { feature } = props;
6179
+ const name = getFeatureName(feature);
6180
+ const id = getFeatureId$1(feature);
6181
+ return (React__default.createElement(Table, { size: "small", sx: { fontSize: '0.75rem', '& .MuiTableCell-root': { padding: '4px' } } },
6182
+ React__default.createElement(TableBody, null,
6183
+ name !== '' && (React__default.createElement(TableRow, null,
6184
+ React__default.createElement(HeaderTableCell, null, "Name"),
6185
+ React__default.createElement(TableCell, null, getFeatureName(feature)))),
6186
+ id !== '' && (React__default.createElement(TableRow, null,
6187
+ React__default.createElement(HeaderTableCell, null, "ID"),
6188
+ React__default.createElement(TableCell, null, getFeatureId$1(feature)))),
6189
+ React__default.createElement(TableRow, null,
6190
+ React__default.createElement(HeaderTableCell, null, "Location"),
6191
+ React__default.createElement(TableCell, null,
6192
+ props.refName,
6193
+ ":",
6194
+ feature.min,
6195
+ "..",
6196
+ feature.max)),
6197
+ React__default.createElement(TableRow, null,
6198
+ React__default.createElement(HeaderTableCell, null, "Strand"),
6199
+ React__default.createElement(TableCell, null, getStrand(feature.strand))))));
6200
+ });
6201
+
6202
+ /* eslint-disable unicorn/no-nested-ternary */
6203
+ const StyledTextField = styled(NumberTextField)(() => ({
6204
+ '&.MuiFormControl-root': {
6205
+ marginTop: 0,
6206
+ marginBottom: 0,
6207
+ width: '100%',
6208
+ },
6209
+ '& .MuiInputBase-input': {
6210
+ fontSize: 12,
6211
+ height: 20,
6212
+ padding: 1,
6213
+ paddingLeft: 10,
6214
+ },
6215
+ }));
6216
+ const SequenceContainer = styled('div')({
6217
+ display: 'flex',
6218
+ justifyContent: 'center',
6219
+ alignItems: 'center',
6220
+ textAlign: 'left',
6221
+ width: '100%',
6222
+ overflowWrap: 'break-word',
6223
+ wordWrap: 'break-word',
6224
+ wordBreak: 'break-all',
6225
+ '& span': {
6226
+ fontSize: 12,
6227
+ },
6228
+ });
6229
+ const Strand = (props) => {
6230
+ const { strand } = props;
6231
+ return (React__default.createElement("div", null, strand === 1 ? (React__default.createElement(AddIcon, null)) : strand === -1 ? (React__default.createElement(RemoveIcon, null)) : (React__default.createElement(Typography, { component: 'span' }, "N/A"))));
6232
+ };
6233
+ const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocation({ assembly, feature, refName, session, }) {
6234
+ const { notify } = session;
6235
+ const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
6236
+ const refData = currentAssembly?.getByRefName(refName);
6237
+ const { changeManager } = session.apolloDataStore;
6238
+ const seqRef = useRef(null);
6239
+ // Separate function to handle CDS location change
6240
+ // because start of CDS and exon might be same
6241
+ function handleCDSLocationChange(oldLocation, newLocation, feature, isMin) {
6242
+ if (!feature.children) {
6243
+ throw new Error('Transcript should have child features');
6244
+ }
6245
+ for (const [, child] of feature.children) {
6246
+ if (child.type !== 'CDS') {
6247
+ continue;
6122
6248
  }
6123
- else {
6124
- locationIntervals.push(previous);
6125
- previous = allLocs[i];
6249
+ if (isMin && oldLocation === child.min) {
6250
+ const change = new LocationStartChange({
6251
+ typeName: 'LocationStartChange',
6252
+ changedIds: [child._id],
6253
+ featureId: feature._id,
6254
+ oldStart: child.min,
6255
+ newStart: newLocation,
6256
+ assembly,
6257
+ });
6258
+ changeManager.submit(change).catch(() => {
6259
+ notify('Error updating feature start position', 'error');
6260
+ });
6261
+ return;
6262
+ }
6263
+ if (!isMin && oldLocation === child.max) {
6264
+ const change = new LocationEndChange({
6265
+ typeName: 'LocationEndChange',
6266
+ changedIds: [child._id],
6267
+ featureId: feature._id,
6268
+ oldEnd: child.max,
6269
+ newEnd: newLocation,
6270
+ assembly,
6271
+ });
6272
+ changeManager.submit(change).catch(() => {
6273
+ notify('Error updating feature start position', 'error');
6274
+ });
6275
+ return;
6126
6276
  }
6127
6277
  }
6128
- locationIntervals.push(previous);
6129
6278
  }
6130
- return (React__default.createElement(React__default.Fragment, null,
6131
- React__default.createElement(Typography, { variant: "h5" }, "Sequence"),
6132
- React__default.createElement("div", null,
6133
- React__default.createElement(Button, { variant: "contained", onClick: handleSeqButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence')),
6134
- showSequence && (React__default.createElement(React__default.Fragment, null,
6135
- React__default.createElement(Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
6136
- React__default.createElement(MenuItem, { value: "CDS" }, "CDS"),
6137
- React__default.createElement(MenuItem, { value: "cDNA" }, "cDNA"),
6138
- React__default.createElement(MenuItem, { value: "genomic" }, "Genomic"),
6139
- React__default.createElement(MenuItem, { value: "protein" }, "Protein")),
6140
- React__default.createElement(Paper, { style: {
6141
- fontFamily: 'monospace',
6142
- padding: theme.spacing(),
6143
- overflowX: 'auto',
6144
- }, ref: seqRef },
6145
- ">",
6146
- refSeq.name,
6147
- ":",
6148
- locationIntervals
6149
- .map((interval) => feature.strand === 1
6150
- ? `${interval.min + 1}-${interval.max}`
6151
- : `${interval.max}-${interval.min + 1}`)
6152
- .join(';'),
6153
- "(",
6154
- feature.strand === 1 ? '+' : '-',
6155
- ")",
6156
- React__default.createElement("br", null),
6157
- sequenceSegments.map((segment, index) => (React__default.createElement("span", { key: `${segment.type}-${index}`, style: {
6158
- background: getSegmentColor(segment.type),
6159
- color: theme.palette.getContrastText(getSegmentColor(segment.type)),
6160
- } }, segment.sequenceLines.map((sequenceLine, idx) => (React__default.createElement(React__default.Fragment, { key: `${sequenceLine.slice(0, 5)}-${idx}` },
6161
- sequenceLine,
6162
- idx === segment.sequenceLines.length - 1 &&
6163
- sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (React__default.createElement("br", null))))))))),
6164
- React__default.createElement(Button, { variant: "contained", onClick: copyToClipboard }, "Copy sequence")))));
6279
+ function handleExonLocationChange(oldLocation, newLocation, feature, isMin) {
6280
+ if (!feature.children) {
6281
+ throw new Error('Transcript should have child features');
6282
+ }
6283
+ for (const [, child] of feature.children) {
6284
+ if (child.type !== 'exon') {
6285
+ continue;
6286
+ }
6287
+ if (isMin && oldLocation === child.min) {
6288
+ const change = new LocationStartChange({
6289
+ typeName: 'LocationStartChange',
6290
+ changedIds: [child._id],
6291
+ featureId: feature._id,
6292
+ oldStart: child.min,
6293
+ newStart: newLocation,
6294
+ assembly,
6295
+ });
6296
+ changeManager.submit(change).catch(() => {
6297
+ notify('Error updating feature start position', 'error');
6298
+ });
6299
+ return;
6300
+ }
6301
+ if (!isMin && oldLocation === child.max) {
6302
+ const change = new LocationEndChange({
6303
+ typeName: 'LocationEndChange',
6304
+ changedIds: [child._id],
6305
+ featureId: feature._id,
6306
+ oldEnd: child.max,
6307
+ newEnd: newLocation,
6308
+ assembly,
6309
+ });
6310
+ changeManager.submit(change).catch(() => {
6311
+ notify('Error updating feature start position', 'error');
6312
+ });
6313
+ return;
6314
+ }
6315
+ }
6316
+ }
6317
+ if (!refData) {
6318
+ return null;
6319
+ }
6320
+ const { cdsLocations, transcriptExonParts, strand } = feature;
6321
+ const [firstCDSLocation] = cdsLocations;
6322
+ const exonParts = transcriptExonParts
6323
+ .filter((part) => part.type === 'exon')
6324
+ .sort(({ min: a }, { min: b }) => a - b);
6325
+ const exonMin = exonParts[0]?.min;
6326
+ const exonMax = exonParts[exonParts.length - 1]?.max;
6327
+ let cdsMin = exonMin;
6328
+ let cdsMax = exonMax;
6329
+ const cdsPresent = firstCDSLocation.length > 0;
6330
+ if (cdsPresent) {
6331
+ cdsMin = firstCDSLocation[0].min;
6332
+ cdsMax = firstCDSLocation[firstCDSLocation.length - 1].max;
6333
+ }
6334
+ const getFivePrimeSpliceSite = (loc, prevLocIdx) => {
6335
+ let spliceSite = '';
6336
+ if (prevLocIdx > 0) {
6337
+ const prevLoc = transcriptExonParts[prevLocIdx - 1];
6338
+ if (strand === 1) {
6339
+ if (prevLoc.type === 'intron') {
6340
+ spliceSite = refData.getSequence(loc.min - 2, loc.min);
6341
+ }
6342
+ }
6343
+ else {
6344
+ if (prevLoc.type === 'intron') {
6345
+ spliceSite = revcom(refData.getSequence(loc.max, loc.max + 2));
6346
+ }
6347
+ }
6348
+ }
6349
+ return [
6350
+ {
6351
+ spliceSite,
6352
+ color: spliceSite === 'AG' ? 'green' : 'red',
6353
+ },
6354
+ ];
6355
+ };
6356
+ const getThreePrimeSpliceSite = (loc, nextLocIdx) => {
6357
+ let spliceSite = '';
6358
+ if (nextLocIdx < transcriptExonParts.length - 1) {
6359
+ const nextLoc = transcriptExonParts[nextLocIdx + 1];
6360
+ if (strand === 1) {
6361
+ if (nextLoc.type === 'intron') {
6362
+ spliceSite = refData.getSequence(loc.max, loc.max + 2);
6363
+ }
6364
+ }
6365
+ else {
6366
+ if (nextLoc.type === 'intron') {
6367
+ spliceSite = revcom(refData.getSequence(loc.min - 2, loc.min));
6368
+ }
6369
+ }
6370
+ }
6371
+ return [
6372
+ {
6373
+ spliceSite,
6374
+ color: spliceSite === 'GT' ? 'green' : 'red',
6375
+ },
6376
+ ];
6377
+ };
6378
+ const getTranslationSequence = () => {
6379
+ let wholeSequence = '';
6380
+ const [firstLocation] = cdsLocations;
6381
+ for (const loc of firstLocation) {
6382
+ let sequence = refData.getSequence(loc.min, loc.max);
6383
+ if (strand === -1) {
6384
+ sequence = revcom(sequence);
6385
+ }
6386
+ wholeSequence += sequence;
6387
+ }
6388
+ const elements = [];
6389
+ for (let codonGenomicPos = 0; codonGenomicPos < wholeSequence.length; codonGenomicPos += 3) {
6390
+ const codonSeq = wholeSequence
6391
+ .slice(codonGenomicPos, codonGenomicPos + 3)
6392
+ .toUpperCase();
6393
+ const protein = defaultCodonTable[codonSeq] || '&';
6394
+ // highlight start codon and stop codons
6395
+ if (codonSeq === 'ATG') {
6396
+ elements.push(React__default.createElement(Typography, { component: 'span', style: {
6397
+ backgroundColor: 'yellow',
6398
+ cursor: 'pointer',
6399
+ border: '1px solid black',
6400
+ }, key: codonGenomicPos, onClick: () => {
6401
+ // NOTE: codonGenomicPos is important here for calculating the genomic location
6402
+ // of the start codon. We are using the codonGenomicPos as the key in the typography
6403
+ // elements to maintain the genomic postion of the codon start
6404
+ const startCodonGenomicLocation = getStartCodonGenomicLocation(codonGenomicPos);
6405
+ if (startCodonGenomicLocation !== cdsMin) {
6406
+ handleCDSLocationChange(cdsMin, startCodonGenomicLocation, feature, true);
6407
+ }
6408
+ } }, protein));
6409
+ }
6410
+ else if (['TAA', 'TAG', 'TGA'].includes(codonSeq)) {
6411
+ elements.push(React__default.createElement(Typography, { style: { backgroundColor: 'red', color: 'white' }, component: 'span',
6412
+ // Pass the codonGenomicPos as the key to maintain the genomic position of the codon
6413
+ key: codonGenomicPos }, protein));
6414
+ }
6415
+ else {
6416
+ elements.push(
6417
+ // Pass the codonGenomicPos as the key to maintain the genomic position of the codon
6418
+ React__default.createElement(Typography, { component: 'span', key: codonGenomicPos }, protein));
6419
+ }
6420
+ }
6421
+ return elements;
6422
+ };
6423
+ // Codon position is the index of the start codon in the CDS genomic sequence
6424
+ // Calculate the genomic location of the start codon based on the codon position in the CDS
6425
+ const getStartCodonGenomicLocation = (codonGenomicPosition) => {
6426
+ const [firstLocation] = cdsLocations;
6427
+ let cdsLen = 0;
6428
+ for (const loc of firstLocation) {
6429
+ const locLength = loc.max - loc.min;
6430
+ // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
6431
+ // and codonGenomicPosition is 25
6432
+ // (((10 - 0) + (30 - 20)) + 10) > 25
6433
+ // 40 + (25-20) = 45 is the genomic location of the start codon
6434
+ if (cdsLen + locLength > codonGenomicPosition) {
6435
+ return loc.min + (codonGenomicPosition - cdsLen);
6436
+ }
6437
+ cdsLen += locLength;
6438
+ }
6439
+ return cdsMin;
6440
+ };
6441
+ const getStopCodonGenomicLocation = (codonGenomicPosition) => {
6442
+ const [firstLocation] = cdsLocations;
6443
+ let cdsLen = 0;
6444
+ for (const loc of firstLocation) {
6445
+ const locLength = loc.max - loc.min;
6446
+ // Check if the codonPosition is within the current location
6447
+ if (cdsLen + locLength > codonGenomicPosition) {
6448
+ return loc.min + (codonGenomicPosition - cdsLen);
6449
+ }
6450
+ cdsLen += locLength;
6451
+ }
6452
+ return cdsMax;
6453
+ };
6454
+ const trimTranslationSequence = () => {
6455
+ const sequenceElements = getTranslationSequence();
6456
+ const translationSequence = sequenceElements
6457
+ .map((el) => el.props.children)
6458
+ .join('');
6459
+ if (translationSequence.startsWith('M') &&
6460
+ translationSequence.endsWith('*')) {
6461
+ return;
6462
+ }
6463
+ // NOTE: We are maintaining the genomic location of the codon start as the "key"
6464
+ // in typography elements. See getTranslationSequence function
6465
+ const translSeqCodonStartGenomicPosArr = [];
6466
+ for (const el of sequenceElements) {
6467
+ translSeqCodonStartGenomicPosArr.push({
6468
+ codonGenomicPos: el.key,
6469
+ sequenceLetter: el.props.children,
6470
+ });
6471
+ }
6472
+ if (translSeqCodonStartGenomicPosArr.length === 0) {
6473
+ return;
6474
+ }
6475
+ // Trim any sequence before first start codon and after last stop codon
6476
+ const startCodonIndex = translationSequence.indexOf('M');
6477
+ const stopCodonIndex = translationSequence.lastIndexOf('*') + 1;
6478
+ const startCodonPos = translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos;
6479
+ const stopCodonPos = translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos;
6480
+ if (!startCodonPos || !stopCodonPos) {
6481
+ return;
6482
+ }
6483
+ const startCodonGenomicLoc = getStartCodonGenomicLocation(startCodonPos);
6484
+ const stopCodonGenomicLoc = getStopCodonGenomicLocation(stopCodonPos);
6485
+ if (startCodonGenomicLoc !== cdsMin) {
6486
+ handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true);
6487
+ }
6488
+ if (stopCodonGenomicLoc !== cdsMax) {
6489
+ // TODO: getting error when trying to change the CDS start and end location at the same time
6490
+ // Need to fix this
6491
+ setTimeout(() => {
6492
+ handleCDSLocationChange(cdsMax, stopCodonGenomicLoc, feature, false);
6493
+ }, 1000);
6494
+ }
6495
+ };
6496
+ const copyToClipboard = () => {
6497
+ const seqDiv = seqRef.current;
6498
+ if (!seqDiv) {
6499
+ return;
6500
+ }
6501
+ const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' });
6502
+ const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
6503
+ const clipboardItem = new ClipboardItem({
6504
+ [textBlob.type]: textBlob,
6505
+ [htmlBlob.type]: htmlBlob,
6506
+ });
6507
+ void navigator.clipboard.write([clipboardItem]);
6508
+ };
6509
+ return (React__default.createElement("div", null,
6510
+ cdsPresent && (React__default.createElement("div", null,
6511
+ React__default.createElement(Accordion, { defaultExpanded: true },
6512
+ React__default.createElement(StyledAccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
6513
+ React__default.createElement(Typography, { component: "span", fontWeight: 'bold' }, "Translation")),
6514
+ React__default.createElement(AccordionDetails, null,
6515
+ React__default.createElement(SequenceContainer, null,
6516
+ React__default.createElement(Typography, { component: 'span', ref: seqRef }, getTranslationSequence())),
6517
+ React__default.createElement("div", { style: {
6518
+ marginTop: 10,
6519
+ display: 'flex',
6520
+ flexDirection: 'row',
6521
+ alignItems: 'center',
6522
+ gap: 10,
6523
+ } },
6524
+ React__default.createElement(Tooltip, { title: "Copy" },
6525
+ React__default.createElement(ContentCopyIcon, { style: { fontSize: 15, cursor: 'pointer' }, onClick: copyToClipboard })),
6526
+ React__default.createElement(Tooltip, { title: "Trim" },
6527
+ React__default.createElement(ContentCutIcon, { style: { fontSize: 15, cursor: 'pointer' }, onClick: trimTranslationSequence }))))),
6528
+ React__default.createElement(Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center', marginTop: 10 } },
6529
+ React__default.createElement(Grid2, { size: 1 }),
6530
+ React__default.createElement(Grid2, { size: 4 },
6531
+ React__default.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin, onChangeCommitted: (newLocation) => {
6532
+ handleCDSLocationChange(cdsMin, newLocation, feature, true);
6533
+ } })),
6534
+ React__default.createElement(Grid2, { size: 2 },
6535
+ React__default.createElement(Typography, { component: 'span' }, "CDS")),
6536
+ React__default.createElement(Grid2, { size: 4 },
6537
+ React__default.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
6538
+ handleCDSLocationChange(cdsMax, newLocation, feature, false);
6539
+ } })),
6540
+ React__default.createElement(Grid2, { size: 1 })))),
6541
+ React__default.createElement("div", { style: { marginTop: 5 } }, transcriptExonParts.map((loc, index) => {
6542
+ return (React__default.createElement("div", { key: index }, loc.type === 'exon' && (React__default.createElement(Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center' } },
6543
+ React__default.createElement(Grid2, { size: 1 }, index !== 0 &&
6544
+ getFivePrimeSpliceSite(loc, index).map((site, idx) => (React__default.createElement(Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite)))),
6545
+ React__default.createElement(Grid2, { size: 4, style: { padding: 0 } },
6546
+ React__default.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min, onChangeCommitted: (newLocation) => {
6547
+ handleExonLocationChange(loc.min, newLocation, feature, true);
6548
+ } })),
6549
+ React__default.createElement(Grid2, { size: 2 },
6550
+ React__default.createElement(Strand, { strand: feature.strand })),
6551
+ React__default.createElement(Grid2, { size: 4, style: { padding: 0 } },
6552
+ React__default.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
6553
+ handleExonLocationChange(loc.max, newLocation, feature, false);
6554
+ } })),
6555
+ React__default.createElement(Grid2, { size: 1 }, index !== transcriptExonParts.length - 1 &&
6556
+ getThreePrimeSpliceSite(loc, index).map((site, idx) => (React__default.createElement(Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite))))))));
6557
+ }))));
6165
6558
  });
6166
6559
 
6167
6560
  const useStyles$7 = makeStyles()((theme) => ({
@@ -6169,10 +6562,24 @@ const useStyles$7 = makeStyles()((theme) => ({
6169
6562
  padding: theme.spacing(2),
6170
6563
  },
6171
6564
  }));
6565
+ const StyledAccordionSummary = styled(AccordionSummary)(() => ({
6566
+ minHeight: 30,
6567
+ maxHeight: 30,
6568
+ '&.Mui-expanded': {
6569
+ minHeight: 30,
6570
+ maxHeight: 30,
6571
+ },
6572
+ }));
6172
6573
  const ApolloTranscriptDetailsWidget = observer(function ApolloTranscriptDetails(props) {
6173
6574
  const { classes } = useStyles$7();
6575
+ const DEFAULT_PANELS = ['summary', 'location', 'attrs'];
6576
+ const [panelState, setPanelState] = useState(DEFAULT_PANELS);
6174
6577
  const { model } = props;
6175
6578
  const { assembly, feature, refName } = model;
6579
+ useEffect(() => {
6580
+ setPanelState(DEFAULT_PANELS);
6581
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6582
+ }, [feature]);
6176
6583
  const session = getSession(model);
6177
6584
  const apolloSession = getSession(model);
6178
6585
  const currentAssembly = apolloSession.apolloDataStore.assemblies.get(assembly);
@@ -6194,12 +6601,47 @@ const ApolloTranscriptDetailsWidget = observer(function ApolloTranscriptDetails(
6194
6601
  { assemblyName: assembly, refName, start: min, end: max },
6195
6602
  ]);
6196
6603
  }
6604
+ function handlePanelChange(expanded, panel) {
6605
+ if (expanded) {
6606
+ setPanelState([...panelState, panel]);
6607
+ }
6608
+ else {
6609
+ setPanelState(panelState.filter((p) => p !== panel));
6610
+ }
6611
+ }
6197
6612
  return (React__default.createElement("div", { className: classes.root },
6198
- React__default.createElement(TranscriptBasicInformation, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName }),
6199
- React__default.createElement("hr", null),
6200
- React__default.createElement(Attributes, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', editable: editable }),
6201
- React__default.createElement("hr", null),
6202
- React__default.createElement(TranscriptSequence, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName })));
6613
+ React__default.createElement(Accordion, { expanded: panelState.includes('summary'), onChange: (e, expanded) => {
6614
+ handlePanelChange(expanded, 'summary');
6615
+ } },
6616
+ React__default.createElement(StyledAccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
6617
+ React__default.createElement(Typography, { component: "span", fontWeight: 'bold' }, "Summary")),
6618
+ React__default.createElement(AccordionDetails, null,
6619
+ React__default.createElement(TranscriptWidgetSummary, { feature: feature, refName: refName }))),
6620
+ React__default.createElement(Accordion, { style: { marginTop: 5 }, expanded: panelState.includes('location'), onChange: (e, expanded) => {
6621
+ handlePanelChange(expanded, 'location');
6622
+ } },
6623
+ React__default.createElement(StyledAccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel2-content", id: "panel2-header" },
6624
+ React__default.createElement(Typography, { component: "span", fontWeight: 'bold' }, "Location")),
6625
+ React__default.createElement(AccordionDetails, null,
6626
+ React__default.createElement(TranscriptWidgetEditLocation, { feature: feature, refName: refName, session: apolloSession, assembly: currentAssembly._id || '' }))),
6627
+ React__default.createElement(Accordion, { style: { marginTop: 5 }, expanded: panelState.includes('attrs'), onChange: (e, expanded) => {
6628
+ handlePanelChange(expanded, 'attrs');
6629
+ } },
6630
+ React__default.createElement(StyledAccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel3-content", id: "panel3-header" },
6631
+ React__default.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
6632
+ React__default.createElement(Typography, { component: "span", fontWeight: 'bold' },
6633
+ "Attributes",
6634
+ ' '),
6635
+ React__default.createElement(Tooltip, { title: "Separate multiple values for the attribute with commas" },
6636
+ React__default.createElement(InfoIcon, { style: { color: 'white', fontSize: 15, marginLeft: 10 } })))),
6637
+ React__default.createElement(AccordionDetails, null,
6638
+ React__default.createElement(Attributes, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', editable: editable }))),
6639
+ React__default.createElement(Accordion, { style: { marginTop: 5 }, expanded: panelState.includes('sequence'), onChange: (e, expanded) => {
6640
+ handlePanelChange(expanded, 'sequence');
6641
+ } },
6642
+ React__default.createElement(StyledAccordionSummary, { expandIcon: React__default.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel4-content", id: "panel4-header" },
6643
+ React__default.createElement(Typography, { component: "span", fontWeight: 'bold' }, "Sequence")),
6644
+ React__default.createElement(AccordionDetails, null, panelState.includes('sequence') && (React__default.createElement(TranscriptSequence, { feature: feature, session: apolloSession, assembly: currentAssembly._id || '', refName: refName }))))));
6203
6645
  });
6204
6646
 
6205
6647
  const configSchema$1 = ConfigurationSchema('LinearApolloDisplay', {}, { explicitIdentifier: 'displayId', explicitlyTyped: true });
@@ -6356,29 +6798,13 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
6356
6798
  },
6357
6799
  ]);
6358
6800
  },
6359
- }, {
6360
- label: 'Edit attributes',
6361
- disabled: readOnly,
6362
- onClick: () => {
6363
- session.queueDialog((doneCallback) => [
6364
- ModifyFeatureAttribute,
6365
- {
6366
- session,
6367
- handleClose: () => {
6368
- doneCallback();
6369
- },
6370
- changeManager,
6371
- sourceFeature: feature,
6372
- sourceAssemblyId: currentAssemblyId,
6373
- },
6374
- ]);
6375
- },
6376
6801
  });
6377
6802
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
6378
6803
  if (!featureTypeOntology) {
6379
6804
  throw new Error('featureTypeOntology is undefined');
6380
6805
  }
6381
- if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
6806
+ if ((featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
6807
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
6382
6808
  isSessionModelWithWidgets(session)) {
6383
6809
  menuItems.push({
6384
6810
  label: 'Edit transcript details',
@@ -6420,6 +6846,12 @@ const NumberCell = observer(function NumberCell({ initialValue, notifyError, onC
6420
6846
  const [blur, setBlur] = useState(false);
6421
6847
  const [inputNode, setInputNode] = useState(null);
6422
6848
  const { classes } = useStyles$5();
6849
+ useEffect(() => {
6850
+ if (initialValue !== value) {
6851
+ setValue(initialValue);
6852
+ }
6853
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6854
+ }, [initialValue]);
6423
6855
  useEffect(() => {
6424
6856
  if (blur) {
6425
6857
  inputNode?.blur();
@@ -6451,7 +6883,7 @@ const NumberCell = observer(function NumberCell({ initialValue, notifyError, onC
6451
6883
  } })));
6452
6884
  });
6453
6885
 
6454
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
6886
+ /* eslint-disable unicorn/no-nested-ternary */
6455
6887
  const useStyles$4 = makeStyles()((theme) => ({
6456
6888
  typeContent: {
6457
6889
  display: 'inline-block',
@@ -6695,12 +7127,14 @@ const ToolBar = observer(function ToolBar({ model: displayState, }) {
6695
7127
  React__default.createElement(UnfoldLessIcon, null))),
6696
7128
  React__default.createElement(TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
6697
7129
  model.setFilterText(event.target.value);
6698
- }, InputProps: {
6699
- endAdornment: (React__default.createElement(InputAdornment$1, { position: "end" },
6700
- React__default.createElement(IconButton, { onClick: () => {
6701
- model.clearFilterText();
6702
- } },
6703
- React__default.createElement(ClearIcon, null)))),
7130
+ }, slotProps: {
7131
+ input: {
7132
+ endAdornment: (React__default.createElement(InputAdornment, { position: "end" },
7133
+ React__default.createElement(IconButton, { onClick: () => {
7134
+ model.clearFilterText();
7135
+ } },
7136
+ React__default.createElement(ClearIcon, null)))),
7137
+ },
6704
7138
  } })));
6705
7139
  });
6706
7140
 
@@ -7254,23 +7688,6 @@ function getContextMenuItems$2(display) {
7254
7688
  },
7255
7689
  ]);
7256
7690
  },
7257
- }, {
7258
- label: 'Modify feature attribute',
7259
- disabled: readOnly,
7260
- onClick: () => {
7261
- session.queueDialog((doneCallback) => [
7262
- ModifyFeatureAttribute,
7263
- {
7264
- session,
7265
- handleClose: () => {
7266
- doneCallback();
7267
- },
7268
- changeManager,
7269
- sourceFeature,
7270
- sourceAssemblyId: currentAssemblyId,
7271
- },
7272
- ]);
7273
- },
7274
7691
  }, {
7275
7692
  label: 'Edit feature details',
7276
7693
  onClick: () => {
@@ -7286,7 +7703,8 @@ function getContextMenuItems$2(display) {
7286
7703
  if (!featureTypeOntology) {
7287
7704
  throw new Error('featureTypeOntology is undefined');
7288
7705
  }
7289
- if (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
7706
+ if ((featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
7707
+ featureTypeOntology.isTypeOf(sourceFeature.type, 'pseudogenic_transcript')) &&
7290
7708
  isSessionModelWithWidgets(session)) {
7291
7709
  menuItems.push({
7292
7710
  label: 'Edit transcript details',
@@ -7466,7 +7884,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7466
7884
  // Draw lines on different rows for each transcript
7467
7885
  let currentRow = 0;
7468
7886
  for (const [, transcript] of children) {
7469
- const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript');
7887
+ const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript') ||
7888
+ featureTypeOntology.isTypeOf(transcript.type, 'pseudogenic_transcript');
7470
7889
  if (!isTranscript) {
7471
7890
  currentRow += 1;
7472
7891
  continue;
@@ -7493,7 +7912,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7493
7912
  // Draw exon and CDS for each transcript
7494
7913
  currentRow = 0;
7495
7914
  for (const [, child] of children) {
7496
- if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
7915
+ if (!(featureTypeOntology.isTypeOf(child.type, 'transcript') ||
7916
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript'))) {
7497
7917
  boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex);
7498
7918
  currentRow += 1;
7499
7919
  continue;
@@ -7683,7 +8103,8 @@ function getFeatureFromLayout$1(feature, bp, row, featureTypeOntology) {
7683
8103
  }
7684
8104
  if (featureTypeOntology.isTypeOf(featureObj.type, 'CDS') &&
7685
8105
  featureObj.parent &&
7686
- featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript')) {
8106
+ (featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript') ||
8107
+ featureTypeOntology.isTypeOf(featureObj.parent.type, 'pseudogenic_transcript'))) {
7687
8108
  const { cdsLocations } = featureObj.parent;
7688
8109
  for (const cdsLoc of cdsLocations) {
7689
8110
  for (const loc of cdsLoc) {
@@ -7705,7 +8126,7 @@ function getCDSCount(feature, featureTypeOntology) {
7705
8126
  if (!children) {
7706
8127
  return 0;
7707
8128
  }
7708
- const isMrna = featureTypeOntology.isTypeOf(type, 'mRNA');
8129
+ const isMrna = featureTypeOntology.isTypeOf(type, 'transcript');
7709
8130
  let cdsCount = 0;
7710
8131
  if (isMrna) {
7711
8132
  for (const [, child] of children) {
@@ -7721,7 +8142,8 @@ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
7721
8142
  if (!children) {
7722
8143
  return 1;
7723
8144
  }
7724
- const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript');
8145
+ const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript') ||
8146
+ featureTypeOntology.isTypeOf(type, 'pseudogenic_transcript');
7725
8147
  let rowCount = 0;
7726
8148
  if (isTranscript) {
7727
8149
  for (const [, child] of children) {
@@ -7744,7 +8166,8 @@ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
7744
8166
  * If the row does not contain an transcript, the order is subfeature -\> gene
7745
8167
  */
7746
8168
  function featuresForRow$1(feature, featureTypeOntology) {
7747
- const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
8169
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') ||
8170
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
7748
8171
  if (!isGene) {
7749
8172
  throw new Error('Top level feature for GeneGlyph must have type "gene"');
7750
8173
  }
@@ -7754,7 +8177,8 @@ function featuresForRow$1(feature, featureTypeOntology) {
7754
8177
  }
7755
8178
  const features = [];
7756
8179
  for (const [, child] of children) {
7757
- if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
8180
+ if (!(featureTypeOntology.isTypeOf(child.type, 'transcript') ||
8181
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript'))) {
7758
8182
  features.push([child, feature]);
7759
8183
  continue;
7760
8184
  }
@@ -7829,8 +8253,10 @@ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7829
8253
  if (!featureTypeOntology) {
7830
8254
  throw new Error('featureTypeOntology is undefined');
7831
8255
  }
7832
- const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
7833
- const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript');
8256
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') ||
8257
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
8258
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
8259
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript');
7834
8260
  const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS');
7835
8261
  if (isGene || isTranscript) {
7836
8262
  return;
@@ -7867,7 +8293,7 @@ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7867
8293
  }
7868
8294
  }
7869
8295
  const overlappingExon = exonChildren.find((child) => {
7870
- const [start, end] = intersection2(bp, bp + 1, child.min, child.max);
8296
+ const [start, end] = intersection2(bp - 1, bp, child.min, child.max);
7871
8297
  return start !== undefined && end !== undefined;
7872
8298
  });
7873
8299
  if (!overlappingExon) {
@@ -8076,12 +8502,14 @@ function layoutsModelFactory(pluginManager, configSchema) {
8076
8502
  if (!children?.size) {
8077
8503
  return false;
8078
8504
  }
8079
- const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
8505
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') ||
8506
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
8080
8507
  if (!isGene) {
8081
8508
  return false;
8082
8509
  }
8083
8510
  for (const [, child] of children) {
8084
- if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
8511
+ if (featureTypeOntology.isTypeOf(child.type, 'transcript') ||
8512
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript')) {
8085
8513
  const { children: grandChildren } = child;
8086
8514
  if (!grandChildren?.size) {
8087
8515
  return false;
@@ -8351,6 +8779,8 @@ function codonColorCode(letter) {
8351
8779
  return colorMap[letter.toUpperCase()];
8352
8780
  }
8353
8781
  function reverseCodonSeq(seq) {
8782
+ // disable because sequence is all ascii
8783
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
8354
8784
  return [...seq]
8355
8785
  .map((c) => revcom(c))
8356
8786
  .reverse()
@@ -8421,6 +8851,8 @@ function sequenceRenderingModelFactory(pluginManager, configSchema) {
8421
8851
  if (!seq) {
8422
8852
  return;
8423
8853
  }
8854
+ // disable because sequence is all ascii
8855
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
8424
8856
  for (const [i, letter] of [...seq].entries()) {
8425
8857
  const trnslXOffset = (self.lgv.bpToPx({
8426
8858
  refName: region.refName,
@@ -8887,7 +9319,7 @@ const useStyles$1 = makeStyles()((theme) => ({
8887
9319
  const LinearApolloDisplay = observer(function LinearApolloDisplay(props) {
8888
9320
  const theme = useTheme();
8889
9321
  const { model } = props;
8890
- const { loading, apolloRowHeight, contextMenuItems: getContextMenuItems, cursor, featuresHeight, isShown, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, regionCannotBeRendered, session, setCanvas, setCollaboratorCanvas, setOverlayCanvas, setSeqTrackCanvas, setSeqTrackOverlayCanvas, setTheme, } = model;
9322
+ const { loading, apolloDragging, apolloRowHeight, contextMenuItems: getContextMenuItems, cursor, featuresHeight, isShown, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, regionCannotBeRendered, session, setCanvas, setCollaboratorCanvas, setOverlayCanvas, setSeqTrackCanvas, setSeqTrackOverlayCanvas, setTheme, } = model;
8891
9323
  const { classes } = useStyles$1();
8892
9324
  const lgv = getContainingView(model);
8893
9325
  useEffect(() => {
@@ -8965,15 +9397,21 @@ const LinearApolloDisplay = observer(function LinearApolloDisplay(props) {
8965
9397
  if (!feature) {
8966
9398
  return null;
8967
9399
  }
8968
- const { topLevelFeature } = feature;
8969
- const row = parent
8970
- ? model.getFeatureLayoutPosition(topLevelFeature)
8971
- ?.layoutRow ?? 0
8972
- : 0;
9400
+ let row = 0;
9401
+ const featureLayout = model.getFeatureLayoutPosition(feature);
9402
+ if (featureLayout) {
9403
+ row = featureLayout.layoutRow + featureLayout.featureRow;
9404
+ }
8973
9405
  const top = row * apolloRowHeight;
8974
9406
  const height = apolloRowHeight;
8975
9407
  return (React__default.createElement(Tooltip, { key: checkResult._id, title: checkResult.message },
8976
- React__default.createElement(Avatar, { className: classes.avatar, style: { top, left, height, width: height } },
9408
+ React__default.createElement(Avatar, { className: classes.avatar, style: {
9409
+ top,
9410
+ left,
9411
+ height,
9412
+ width: height,
9413
+ pointerEvents: apolloDragging ? 'none' : 'auto',
9414
+ } },
8977
9415
  React__default.createElement(ErrorIcon, null))));
8978
9416
  });
8979
9417
  }),
@@ -9092,22 +9530,22 @@ const DisplayComponent = observer(function DisplayComponent({ model, ...other })
9092
9530
  const { featureTypeOntology } = ontologyManager;
9093
9531
  const ontologyStore = featureTypeOntology?.dataStore;
9094
9532
  const { classes } = useStyles();
9095
- const { detailsHeight, graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
9533
+ const { graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
9096
9534
  const canvasScrollContainerRef = useRef(null);
9097
9535
  useEffect(() => {
9098
9536
  scrollSelectedFeatureIntoView(model, canvasScrollContainerRef);
9099
9537
  }, [model, selectedFeature]);
9100
9538
  const onDetailsResize = (delta) => {
9101
- model.setDetailsHeight(detailsHeight - delta);
9539
+ model.setDetailsHeight(model.detailsHeight - delta);
9102
9540
  };
9103
9541
  if (!ontologyStore) {
9104
9542
  return (React__default.createElement("div", { className: classes.alertContainer },
9105
9543
  React__default.createElement(Alert, { severity: "error" }, "Could not load feature type ontology.")));
9106
9544
  }
9107
9545
  if (graphical && table) {
9108
- const tabularHeight = tabularEditor.isShown ? detailsHeight : 0;
9546
+ const tabularHeight = tabularEditor.isShown ? model.detailsHeight : 0;
9109
9547
  const featureAreaHeight = isShown
9110
- ? overallHeight - detailsHeight - accordionControlHeight * 2
9548
+ ? overallHeight - model.detailsHeight - accordionControlHeight * 2
9111
9549
  : 0;
9112
9550
  return (React__default.createElement("div", { style: { height: overallHeight } },
9113
9551
  React__default.createElement(AccordionControl, { open: isShown, title: "Graphical", onClick: toggleShown }),
@@ -10652,7 +11090,8 @@ function stateModelFactory(pluginManager, configSchema) {
10652
11090
  return start1 - start2 || end1 - end2;
10653
11091
  })) {
10654
11092
  for (const [, childFeature] of feature.children ?? new Map()) {
10655
- if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript')) {
11093
+ if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript') ||
11094
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) {
10656
11095
  for (const [, grandChildFeature] of childFeature.children ||
10657
11096
  new Map()) {
10658
11097
  let startingRow;