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