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