@datagrok/bio 2.13.3 → 2.13.6
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/.eslintrc.json +1 -1
- package/CHANGELOG.md +23 -0
- package/detectors.js +52 -38
- package/dist/111.js +2 -0
- package/dist/111.js.map +1 -0
- package/dist/234.js +2 -0
- package/dist/234.js.map +1 -0
- package/dist/242.js +2 -0
- package/dist/242.js.map +1 -0
- package/dist/{286.js → 248.js} +1 -1
- package/dist/248.js.map +1 -0
- package/dist/284.js +3 -0
- package/dist/284.js.map +1 -0
- package/dist/317.js +2 -0
- package/dist/317.js.map +1 -0
- package/dist/589.js +2 -0
- package/dist/589.js.map +1 -0
- package/dist/603.js +2 -0
- package/dist/603.js.map +1 -0
- package/dist/682.js +2 -0
- package/dist/682.js.map +1 -0
- package/dist/705.js +2 -0
- package/dist/705.js.map +1 -0
- package/dist/{590.js → 731.js} +2 -2
- package/dist/731.js.map +1 -0
- package/dist/778.js +2 -0
- package/dist/778.js.map +1 -0
- package/dist/793.js +2 -0
- package/dist/793.js.map +1 -0
- package/dist/950.js +2 -0
- package/dist/950.js.map +1 -0
- package/dist/package-test.js +6 -7
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +6 -7
- package/dist/package.js.map +1 -1
- package/files/cache_config.json +7 -0
- package/package.json +12 -11
- package/src/analysis/sequence-activity-cliffs.ts +1 -1
- package/src/function-edtiors/split-to-monomers-editor.ts +6 -7
- package/src/package-types.ts +14 -7
- package/src/package.ts +6 -6
- package/src/substructure-search/substructure-search.ts +9 -10
- package/src/tests/WebLogo-positions-test.ts +6 -6
- package/src/tests/activity-cliffs-tests.ts +5 -2
- package/src/tests/bio-tests.ts +6 -6
- package/src/tests/checkInputColumn-tests.ts +3 -3
- package/src/tests/converters-test.ts +1 -1
- package/src/tests/detectors-tests.ts +25 -13
- package/src/tests/fasta-export-tests.ts +2 -2
- package/src/tests/mm-distance-tests.ts +1 -1
- package/src/tests/msa-tests.ts +2 -2
- package/src/tests/renderers-test.ts +5 -5
- package/src/tests/scoring.ts +26 -5
- package/src/tests/seq-handler-get-region.ts +4 -4
- package/src/tests/sequence-space-test.ts +1 -1
- package/src/tests/substructure-filters-tests.ts +4 -1
- package/src/tests/to-atomic-level-tests.ts +1 -1
- package/src/utils/cell-renderer.ts +4 -4
- package/src/utils/context-menu.ts +1 -1
- package/src/utils/convert.ts +7 -4
- package/src/utils/get-region-func-editor.ts +11 -16
- package/src/utils/get-region.ts +5 -5
- package/src/utils/macromolecule-column-widget.ts +1 -1
- package/src/utils/monomer-lib/lib-manager.ts +20 -8
- package/src/utils/monomer-lib/library-file-manager/file-manager.ts +28 -24
- package/src/utils/monomer-lib/library-file-manager/file-validator.ts +2 -1
- package/src/utils/monomer-lib/library-file-manager/ui.ts +3 -6
- package/src/utils/multiple-sequence-alignment-ui.ts +10 -11
- package/src/utils/multiple-sequence-alignment.ts +2 -2
- package/src/utils/pepsea.ts +1 -1
- package/src/utils/save-as-fasta.ts +5 -5
- package/src/viewers/vd-regions-viewer.ts +2 -2
- package/src/widgets/bio-substructure-filter.ts +7 -7
- package/src/widgets/package-settings-editor-widget.ts +6 -6
- package/src/widgets/representations.ts +1 -1
- package/tsconfig.json +4 -4
- package/dist/23.js +0 -2
- package/dist/23.js.map +0 -1
- package/dist/231.js +0 -2
- package/dist/231.js.map +0 -1
- package/dist/282.js +0 -2
- package/dist/282.js.map +0 -1
- package/dist/286.js.map +0 -1
- package/dist/356.js +0 -2
- package/dist/356.js.map +0 -1
- package/dist/36.js +0 -2
- package/dist/36.js.map +0 -1
- package/dist/40.js +0 -2
- package/dist/40.js.map +0 -1
- package/dist/413.js +0 -2
- package/dist/413.js.map +0 -1
- package/dist/42.js +0 -2
- package/dist/42.js.map +0 -1
- package/dist/427.js +0 -2
- package/dist/427.js.map +0 -1
- package/dist/545.js +0 -3
- package/dist/545.js.map +0 -1
- package/dist/590.js.map +0 -1
- package/dist/65.js +0 -2
- package/dist/65.js.map +0 -1
- package/dist/796.js +0 -2
- package/dist/796.js.map +0 -1
- package/dist/package-test.js.LICENSE.txt +0 -1
- package/dist/package.js.LICENSE.txt +0 -1
- /package/dist/{545.js.LICENSE.txt → 284.js.LICENSE.txt} +0 -0
package/src/tests/scoring.ts
CHANGED
|
@@ -2,7 +2,9 @@ import * as grok from 'datagrok-api/grok';
|
|
|
2
2
|
import * as ui from 'datagrok-api/ui';
|
|
3
3
|
import * as DG from 'datagrok-api/dg';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import wu from 'wu';
|
|
6
|
+
|
|
7
|
+
import {category, test, expectFloat, before, after, expect} from '@datagrok-libraries/utils/src/test';
|
|
6
8
|
import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
7
9
|
import {IMonomerLibHelper} from '@datagrok-libraries/bio/src/monomer-works/monomer-utils';
|
|
8
10
|
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
import {UserLibSettings} from '@datagrok-libraries/bio/src/monomer-works/types';
|
|
14
16
|
|
|
15
17
|
category('Scoring', () => {
|
|
18
|
+
/* eslint-disable max-len */
|
|
16
19
|
const sequence = 'sequence';
|
|
17
20
|
const expectedSimilarity = 'expected_similarity';
|
|
18
21
|
const expectedIdentity = 'expected_identity';
|
|
@@ -22,11 +25,13 @@ PEPTIDE1{Aca.Orn.gGlu.Pqa.D-His_1Bn.dH.hHis.4Abz.D-Tic.D-Dap.Y.Iva.meS.F.P.F.D-1
|
|
|
22
25
|
PEPTIDE1{Iva.Gly_allyl.gGlu.Pqa.D-Dip.dH.hHis.4Abz.D-aHyp.D-Dap.Y.Iva.I.Tyr_26diMe.P.Asu.meC}$$$$,0.68,0.53
|
|
23
26
|
PEPTIDE1{[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal]}$$$$V2.0,0.34,0.0`
|
|
24
27
|
);
|
|
25
|
-
/* eslint-enable max-len */
|
|
26
28
|
const seqCol: DG.Column<string> = table.getCol(sequence);
|
|
27
|
-
seqCol.
|
|
29
|
+
seqCol.meta.units = NOTATION.HELM;
|
|
28
30
|
seqCol.semType = DG.SEMTYPE.MACROMOLECULE;
|
|
29
31
|
const reference = seqCol.get(0)!;
|
|
32
|
+
const shortReference = 'PEPTIDE1{Iva.Gly_allyl.gGlu.Pqa.D-Dip.dH.hHis.4Abz.D-aHyp.D-Dap.Y.Iva}$$$$';
|
|
33
|
+
const longReference = 'PEPTIDE1{Iva.Gly_allyl.gGlu.Pqa.D-Dip.dH.hHis.4Abz.D-aHyp.D-Dap.Y.Iva.I.Tyr_26diMe.P.Asu.meC.I.Tyr_26diMe.P.Asu.meC}$$$$';
|
|
34
|
+
/* eslint-enable max-len */
|
|
30
35
|
|
|
31
36
|
let monomerLibHelper: IMonomerLibHelper;
|
|
32
37
|
/** Backup actual user's monomer libraries settings */
|
|
@@ -49,15 +54,31 @@ PEPTIDE1{[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[1Nal].[
|
|
|
49
54
|
test('Identity', async () => {
|
|
50
55
|
const scoresCol = await sequenceIdentityScoring(table, seqCol, reference);
|
|
51
56
|
for (let i = 0; i < scoresCol.length; i++) {
|
|
52
|
-
|
|
57
|
+
const resScore = scoresCol.get(i)!;
|
|
58
|
+
const tgtScore = table.get(expectedIdentity, i);
|
|
59
|
+
expectFloat(resScore, tgtScore, 0.01,
|
|
53
60
|
`Wrong identity score for sequence at position ${i}`);
|
|
54
61
|
}
|
|
55
62
|
});
|
|
56
63
|
|
|
64
|
+
test('Identity-shortReference', async () => {
|
|
65
|
+
const scoresCol = await sequenceIdentityScoring(table, seqCol, shortReference);
|
|
66
|
+
expect(wu.count(0).take(scoresCol.length).map((rowI) => scoresCol.get(rowI))
|
|
67
|
+
.every((v) => v != null && !isNaN(v)), true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('Identity-longReference', async () => {
|
|
71
|
+
const scoresCol = await sequenceIdentityScoring(table, seqCol, longReference);
|
|
72
|
+
expect(wu.count(0).take(scoresCol.length).map((rowI) => scoresCol.get(rowI))
|
|
73
|
+
.every((v) => v != null && !isNaN(v)), true);
|
|
74
|
+
});
|
|
75
|
+
|
|
57
76
|
test('Similarity', async () => {
|
|
58
77
|
const scoresCol = await sequenceSimilarityScoring(table, seqCol, reference);
|
|
59
78
|
for (let i = 0; i < scoresCol.length; i++) {
|
|
60
|
-
|
|
79
|
+
const resScore = scoresCol.get(i)!;
|
|
80
|
+
const tgtScore = table.get(expectedSimilarity, i);
|
|
81
|
+
expectFloat(resScore, tgtScore, 0.01,
|
|
61
82
|
`Wrong similarity score for sequence at position ${i}`);
|
|
62
83
|
}
|
|
63
84
|
});
|
|
@@ -82,8 +82,8 @@ PEPTIDE1{[Cys_SEt].T.*.*}$$$$`,
|
|
|
82
82
|
const tgtDf = DG.DataFrame.fromCsv(testData.tgtCsv);
|
|
83
83
|
const tgtSeqCol = tgtDf.getCol('seq');
|
|
84
84
|
|
|
85
|
-
expect(srcSeqCol.
|
|
86
|
-
expect(resSeqCol.
|
|
85
|
+
expect(srcSeqCol.meta.units, testData.units);
|
|
86
|
+
expect(resSeqCol.meta.units, testData.units);
|
|
87
87
|
expect(srcSeqCol.getTag(TAGS.alphabet), testData.alphabet);
|
|
88
88
|
expect(resSeqCol.getTag(TAGS.alphabet), testData.alphabet);
|
|
89
89
|
expectArray(resSeqCol.toList(), tgtSeqCol.toList());
|
|
@@ -105,8 +105,8 @@ PEPTIDE1{[Cys_SEt].T.*.*}$$$$`,
|
|
|
105
105
|
const tgtDf = DG.DataFrame.fromCsv(testData.tgtCsv);
|
|
106
106
|
const tgtSeqCol = tgtDf.getCol('seq');
|
|
107
107
|
|
|
108
|
-
expect(srcSeqCol.
|
|
109
|
-
expect(resSeqCol.
|
|
108
|
+
expect(srcSeqCol.meta.units, testData.units);
|
|
109
|
+
expect(resSeqCol.meta.units, testData.units);
|
|
110
110
|
expect(srcSeqCol.getTag(TAGS.alphabet), testData.alphabet);
|
|
111
111
|
expect(resSeqCol.getTag(TAGS.alphabet), testData.alphabet);
|
|
112
112
|
expectArray(resSeqCol.toList(), tgtSeqCol.toList());
|
|
@@ -21,7 +21,7 @@ category('sequenceSpace', async () => {
|
|
|
21
21
|
await _testSequenceSpaceReturnsResult(testFastaDf, DimReductionMethods.UMAP, 'sequence');
|
|
22
22
|
//grok.shell.closeTable(testFastaDf);
|
|
23
23
|
//testFastaTableView.close();
|
|
24
|
-
});
|
|
24
|
+
}, {benchmark: true});
|
|
25
25
|
|
|
26
26
|
test('sequenceSpaceWithEmptyRows', async () => {
|
|
27
27
|
testHelmWithEmptyRows = await readDataframe('tests/100_3_clustests_empty_vals.csv');
|
|
@@ -5,15 +5,16 @@ import * as DG from 'datagrok-api/dg';
|
|
|
5
5
|
import $ from 'cash-dom';
|
|
6
6
|
import wu from 'wu';
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
import {after, before, category, test, expect, delay, testEvent, awaitCheck} from '@datagrok-libraries/utils/src/test';
|
|
10
9
|
import {getMonomerLibHelper, IMonomerLibHelper} from '@datagrok-libraries/bio/src/monomer-works/monomer-utils';
|
|
11
10
|
import {
|
|
12
11
|
getUserLibSettings, setUserLibSettings, setUserLibSettingsForTests
|
|
13
12
|
} from '@datagrok-libraries/bio/src/monomer-works/lib-settings';
|
|
14
13
|
import {UserLibSettings} from '@datagrok-libraries/bio/src/monomer-works/types';
|
|
14
|
+
import {getHelmHelper, IHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
|
|
15
15
|
|
|
16
16
|
import {awaitGrid, readDataframe} from './utils';
|
|
17
|
+
|
|
17
18
|
import {
|
|
18
19
|
BioSubstructureFilter, FastaBioFilter, SeparatorBioFilter, SeparatorFilterProps
|
|
19
20
|
} from '../widgets/bio-substructure-filter';
|
|
@@ -24,11 +25,13 @@ import {_package} from '../package-test';
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
category('bio-substructure-filters', async () => {
|
|
28
|
+
let helmHelper: IHelmHelper;
|
|
27
29
|
let monomerLibHelper: IMonomerLibHelper;
|
|
28
30
|
/** Backup actual user's monomer libraries settings */
|
|
29
31
|
let userLibSettings: UserLibSettings;
|
|
30
32
|
|
|
31
33
|
before(async () => {
|
|
34
|
+
helmHelper = await getHelmHelper(); // init Helm package
|
|
32
35
|
monomerLibHelper = await getMonomerLibHelper();
|
|
33
36
|
userLibSettings = await getUserLibSettings();
|
|
34
37
|
|
|
@@ -205,7 +205,7 @@ PEPTIDE1{Lys_Boc.hHis.Aca.Cys_SEt.T.dK.Thr_PO3H2.Aca.Tyr_PO3H2.Thr_PO3H2.Aca.Tyr
|
|
|
205
205
|
const srcDf = DG.DataFrame.fromCsv(srcCsv);
|
|
206
206
|
const seqCol = srcDf.getCol('seq');
|
|
207
207
|
seqCol.semType = DG.SEMTYPE.MACROMOLECULE;
|
|
208
|
-
seqCol.
|
|
208
|
+
seqCol.meta.units = NOTATION.FASTA;
|
|
209
209
|
seqCol.setTag(bioTAGS.alphabet, ALPHABET.PT);
|
|
210
210
|
const sh = SeqHandler.forColumn(seqCol);
|
|
211
211
|
const resCol = (await _testToAtomicLevel(srcDf, 'seq', monomerLibHelper))!;
|
|
@@ -154,7 +154,7 @@ export class MacromoleculeSequenceCellRenderer extends DG.GridCellRenderer {
|
|
|
154
154
|
const msaGapLength = 8;
|
|
155
155
|
|
|
156
156
|
// Cell renderer settings
|
|
157
|
-
let maxLengthOfMonomer: number = (_package.properties ? _package.properties.
|
|
157
|
+
let maxLengthOfMonomer: number = (_package.properties ? _package.properties.maxMonomerLength : 4) ?? 50;
|
|
158
158
|
if (mmcrTAGS.maxMonomerLength in tableCol.tags) {
|
|
159
159
|
const v = parseInt(tableCol.getTag(mmcrTAGS.maxMonomerLength));
|
|
160
160
|
maxLengthOfMonomer = !isNaN(v) && v ? v : 50;
|
|
@@ -209,7 +209,7 @@ export class MacromoleculeSequenceCellRenderer extends DG.GridCellRenderer {
|
|
|
209
209
|
g.textBaseline = 'top';
|
|
210
210
|
|
|
211
211
|
//TODO: can this be replaced/merged with splitSequence?
|
|
212
|
-
const units = tableCol.
|
|
212
|
+
const units = tableCol.meta.units;
|
|
213
213
|
const aligned: string = tableCol.getTag(bioTAGS.aligned);
|
|
214
214
|
|
|
215
215
|
const palette = getPaletteByType(paletteType);
|
|
@@ -298,7 +298,7 @@ export class MacromoleculeDifferenceCellRenderer extends DG.GridCellRenderer {
|
|
|
298
298
|
const tableCol = gridCell.tableColumn as DG.Column<string>;
|
|
299
299
|
const s: string = cell.value ?? '';
|
|
300
300
|
const separator = tableCol.tags[bioTAGS.separator];
|
|
301
|
-
const units: string = tableCol.
|
|
301
|
+
const units: string = tableCol.meta.units!;
|
|
302
302
|
w = getUpdatedWidth(grid, g, x, w, dpr);
|
|
303
303
|
//TODO: can this be replaced/merged with splitSequence?
|
|
304
304
|
const [s1, s2] = s.split('#');
|
|
@@ -345,7 +345,7 @@ export function drawMoleculeDifferenceOnCanvas(
|
|
|
345
345
|
g.textBaseline = 'top';
|
|
346
346
|
|
|
347
347
|
let palette: SeqPalette = UnknownSeqPalettes.Color;
|
|
348
|
-
if (units
|
|
348
|
+
if (units !== 'HELM')
|
|
349
349
|
palette = getPaletteByType(units.substring(units.length - 2));
|
|
350
350
|
|
|
351
351
|
const vShift = 7;
|
|
@@ -17,7 +17,7 @@ export function addCopyMenuUI(cell: DG.Cell, menu: DG.Menu): void {
|
|
|
17
17
|
const srcCol = cell.column;
|
|
18
18
|
const srcRowIdx = cell.rowIndex;
|
|
19
19
|
const srcSh = SeqHandler.forColumn(srcCol);
|
|
20
|
-
const separator = tgtNotation === NOTATION.SEPARATOR ? _package.properties.
|
|
20
|
+
const separator = tgtNotation === NOTATION.SEPARATOR ? _package.properties.defaultSeparator : undefined;
|
|
21
21
|
const joiner = srcSh.getJoiner({notation: tgtNotation as NOTATION, separator});
|
|
22
22
|
const srcSS = srcSh.getSplitted(srcRowIdx);
|
|
23
23
|
const tgtSeq = joiner(srcSS);
|
package/src/utils/convert.ts
CHANGED
|
@@ -52,7 +52,8 @@ export function convert(col?: DG.Column): void {
|
|
|
52
52
|
separatorInput.value = '/'; // helm monomers can have - in the name like D-aThr;
|
|
53
53
|
dialogHeader.textContent = 'Current notation: ' + currentNotation;
|
|
54
54
|
filteredNotations = notations.filter((e) => e !== currentNotation);
|
|
55
|
-
targetNotationInput = ui.
|
|
55
|
+
targetNotationInput = ui.input.choice('Convert to', {value: filteredNotations[0], items: filteredNotations,
|
|
56
|
+
onValueChanged: toggleSeparator});
|
|
56
57
|
toggleSeparator();
|
|
57
58
|
convertDialog?.clear();
|
|
58
59
|
convertDialog?.add(ui.div([
|
|
@@ -63,12 +64,13 @@ export function convert(col?: DG.Column): void {
|
|
|
63
64
|
]));
|
|
64
65
|
};
|
|
65
66
|
|
|
66
|
-
const targetColumnInput = ui.
|
|
67
|
+
const targetColumnInput = ui.input.column('Column', {table: grok.shell.t, value: srcCol,
|
|
68
|
+
onValueChanged: (input) => toggleColumn(input.value)});
|
|
67
69
|
|
|
68
70
|
const separatorArray = ['-', '.', '/'];
|
|
69
71
|
let filteredNotations = notations.filter((e) => e !== currentNotation);
|
|
70
72
|
|
|
71
|
-
const separatorInput = ui.
|
|
73
|
+
const separatorInput = ui.input.choice('Separator', {value: separatorArray[0], items: separatorArray});
|
|
72
74
|
|
|
73
75
|
// hide the separator input for non-SEPARATOR target notations
|
|
74
76
|
const toggleSeparator = () => {
|
|
@@ -77,7 +79,8 @@ export function convert(col?: DG.Column): void {
|
|
|
77
79
|
else
|
|
78
80
|
$(separatorInput.root).show();
|
|
79
81
|
};
|
|
80
|
-
let targetNotationInput = ui.
|
|
82
|
+
let targetNotationInput = ui.input.choice('Convert to', {value: filteredNotations[0], items: filteredNotations,
|
|
83
|
+
onValueChanged: toggleSeparator});
|
|
81
84
|
|
|
82
85
|
// set correct visibility on init
|
|
83
86
|
toggleSeparator();
|
|
@@ -37,25 +37,20 @@ export class GetRegionFuncEditor {
|
|
|
37
37
|
) {
|
|
38
38
|
const getDesc = (paramName: string) => this.call.inputParams[paramName].property.description;
|
|
39
39
|
|
|
40
|
-
this.inputs.table = ui.
|
|
41
|
-
this.call.inputParams['table'].value ?? grok.shell.tv.dataFrame, undefined,
|
|
42
|
-
() => {});
|
|
40
|
+
this.inputs.table = ui.input.table('Table', {value: this.call.inputParams['table'].value ?? grok.shell.tv.dataFrame});
|
|
43
41
|
|
|
44
42
|
const seqColValue = this.call.inputParams['sequence'].value ??
|
|
45
43
|
this.inputs.table.value!.columns.bySemType(DG.SEMTYPE.MACROMOLECULE);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.inputs.
|
|
50
|
-
|
|
51
|
-
this.inputs.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.inputs.
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
this.inputs.name = ui.stringInput('Column name', this.getDefaultName(),
|
|
58
|
-
this.nameInputChanged.bind(this), {clearIcon: true});
|
|
44
|
+
this.inputs.sequence = ui.input.column('Sequence', {table: grok.shell.tv.dataFrame, value: seqColValue,
|
|
45
|
+
onValueChanged: this.sequenceInputChanged.bind(this), filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE});
|
|
46
|
+
this.inputs.start = ui.input.choice('Start', {onValueChanged: this.startInputChanged.bind(this)}) as unknown as DG.InputBase<string>;
|
|
47
|
+
this.inputs.end = ui.input.choice('End', {onValueChanged: this.endInputChanged.bind(this)}) as unknown as DG.InputBase<string>;
|
|
48
|
+
|
|
49
|
+
this.inputs.region = ui.input.choice<SeqRegion>('Region', {value: null as unknown as SeqRegion, items: [],
|
|
50
|
+
onValueChanged: this.regionInputChanged.bind(this)}) as DG.InputBase<SeqRegion>;
|
|
51
|
+
|
|
52
|
+
this.inputs.name = ui.input.string('Column name', {value: this.getDefaultName(),
|
|
53
|
+
onValueChanged: this.nameInputChanged.bind(this), clearIcon: true});
|
|
59
54
|
this.inputs.name.onInput(this.nameInputInput.bind(this)); // To catch clear event
|
|
60
55
|
|
|
61
56
|
// tooltips
|
package/src/utils/get-region.ts
CHANGED
|
@@ -8,11 +8,11 @@ import {TaskBarProgressIndicator} from 'datagrok-api/dg';
|
|
|
8
8
|
export function getRegionUI(col: DG.Column<string>): void {
|
|
9
9
|
const sh = SeqHandler.forColumn(col);
|
|
10
10
|
|
|
11
|
-
const nameInput = ui.
|
|
12
|
-
const startPositionInput = ui.
|
|
13
|
-
() => { /* TODO: update name placeholder with getDefaultName() */ });
|
|
14
|
-
const endPositionInput = ui.
|
|
15
|
-
() => { /* TODO: update name placeholder with getDefaultName() */ });
|
|
11
|
+
const nameInput = ui.input.string('Name', {value: ''});
|
|
12
|
+
const startPositionInput = ui.input.choice('Start Position', {value: sh.posList[0], items: sh.posList,
|
|
13
|
+
onValueChanged: () => { /* TODO: update name placeholder with getDefaultName() */ }});
|
|
14
|
+
const endPositionInput = ui.input.choice('End Position', {value: sh.posList[sh.posList.length], items: sh.posList,
|
|
15
|
+
onValueChanged: () => { /* TODO: update name placeholder with getDefaultName() */ }});
|
|
16
16
|
|
|
17
17
|
const getDefaultName = (): string => {
|
|
18
18
|
return `${col.name}:${startPositionInput.value}-${endPositionInput.value}`;
|
|
@@ -25,7 +25,7 @@ export class MacromoleculeColumnWidget extends DG.Widget {
|
|
|
25
25
|
|
|
26
26
|
async init(): Promise<void> {
|
|
27
27
|
const sh = SeqHandler.forColumn(this.seqCol);
|
|
28
|
-
const pkgTooltipWebLogo = _package.properties.
|
|
28
|
+
const pkgTooltipWebLogo = _package.properties.tooltipWebLogo;
|
|
29
29
|
const colTooltipWebLogo = this.seqCol.getTag(wlTAGS.tooltipWebLogo);
|
|
30
30
|
|
|
31
31
|
if (pkgTooltipWebLogo !== false && !['false', 'off', 'disable', 'disabled'].includes(colTooltipWebLogo)) {
|
|
@@ -4,6 +4,7 @@ import * as ui from 'datagrok-api/ui';
|
|
|
4
4
|
import * as DG from 'datagrok-api/dg';
|
|
5
5
|
|
|
6
6
|
import {delay} from '@datagrok-libraries/utils/src/test';
|
|
7
|
+
import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
|
|
7
8
|
import {IMonomerLib} from '@datagrok-libraries/bio/src/types';
|
|
8
9
|
import {
|
|
9
10
|
getUserLibSettings, setUserLibSettings, LIB_PATH
|
|
@@ -49,7 +50,9 @@ export class MonomerLibManager implements IMonomerLibHelper {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/** Protect constructor to prevent multiple instantiation. */
|
|
52
|
-
protected constructor(
|
|
53
|
+
protected constructor(
|
|
54
|
+
private readonly logger: ILogger,
|
|
55
|
+
) {}
|
|
53
56
|
|
|
54
57
|
/** Singleton monomer library
|
|
55
58
|
* @return {MonomerLibManager} MonomerLibHelper instance
|
|
@@ -64,7 +67,8 @@ export class MonomerLibManager implements IMonomerLibHelper {
|
|
|
64
67
|
async getFileManager(): Promise<MonomerLibFileManager> {
|
|
65
68
|
if (this._fileManagerPromise === undefined) {
|
|
66
69
|
this._fileManagerPromise = (async () => {
|
|
67
|
-
const fileManager: MonomerLibFileManager =
|
|
70
|
+
const fileManager: MonomerLibFileManager =
|
|
71
|
+
await MonomerLibFileManager.create(this, this._eventManager, this.logger);
|
|
68
72
|
return fileManager;
|
|
69
73
|
})();
|
|
70
74
|
}
|
|
@@ -82,6 +86,7 @@ export class MonomerLibManager implements IMonomerLibHelper {
|
|
|
82
86
|
// WARNING: This function is not allowed to throw any exception,
|
|
83
87
|
// because it will prevent further handling monomer library settings
|
|
84
88
|
// through blocking this.loadLibrariesPromise
|
|
89
|
+
const pi = DG.TaskBarProgressIndicator.create('Loading monomers ...');
|
|
85
90
|
try {
|
|
86
91
|
const [libFileNameList, settings]: [string[], UserLibSettings] = await Promise.all([
|
|
87
92
|
(await this.getFileManager()).getValidLibraryPaths(),
|
|
@@ -95,14 +100,19 @@ export class MonomerLibManager implements IMonomerLibHelper {
|
|
|
95
100
|
return isFileIncluded && isExplicit;
|
|
96
101
|
});
|
|
97
102
|
|
|
103
|
+
let completedLibCount: number = 0;
|
|
98
104
|
const libs: IMonomerLib[] = await Promise.all(filteredLibFnList
|
|
99
105
|
.map((libFileName) => {
|
|
100
106
|
//TODO handle whether files are in place
|
|
101
|
-
return this.readLibrary(LIB_PATH, libFileName)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
return this.readLibrary(LIB_PATH, libFileName)
|
|
108
|
+
.catch((err: any) => {
|
|
109
|
+
const errMsg: string = `Loading monomers from '${libFileName}' error: ` +
|
|
110
|
+
`${err instanceof Error ? err.message : err.toString()}`;
|
|
111
|
+
return new MonomerLib({}, libFileName, errMsg);
|
|
112
|
+
}).finally(() => {
|
|
113
|
+
pi.update(Math.round(100 * (++completedLibCount) / filteredLibFnList.length),
|
|
114
|
+
`Loading monomer libs ${completedLibCount}/${filteredLibFnList.length}`);
|
|
115
|
+
});
|
|
106
116
|
}));
|
|
107
117
|
this._monomerLib.updateLibs(libs, reload);
|
|
108
118
|
} catch (err: any) {
|
|
@@ -112,6 +122,8 @@ export class MonomerLibManager implements IMonomerLibHelper {
|
|
|
112
122
|
|
|
113
123
|
const errStack = err instanceof Error ? err.stack : undefined;
|
|
114
124
|
_package.logger.error(errMsg, undefined, errStack);
|
|
125
|
+
} finally {
|
|
126
|
+
pi.close();
|
|
115
127
|
}
|
|
116
128
|
});
|
|
117
129
|
}
|
|
@@ -149,7 +161,7 @@ export class MonomerLibManager implements IMonomerLibHelper {
|
|
|
149
161
|
let res = window.$monomerLibHelperPromise;
|
|
150
162
|
if (res === undefined) {
|
|
151
163
|
res = window.$monomerLibHelperPromise = (async () => {
|
|
152
|
-
const instance = new MonomerLibManager();
|
|
164
|
+
const instance = new MonomerLibManager(_package.logger);
|
|
153
165
|
instance._eventManager = MonomerLibFileEventManager.getInstance();
|
|
154
166
|
return instance;
|
|
155
167
|
})();
|
|
@@ -6,6 +6,7 @@ import * as DG from 'datagrok-api/dg';
|
|
|
6
6
|
import {JSONSchemaType} from 'ajv';
|
|
7
7
|
|
|
8
8
|
import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types';
|
|
9
|
+
import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
|
|
9
10
|
import {LIB_PATH} from '@datagrok-libraries/bio/src/monomer-works/lib-settings';
|
|
10
11
|
import {
|
|
11
12
|
HELM_REQUIRED_FIELD as REQ,
|
|
@@ -31,6 +32,7 @@ export class MonomerLibFileManager implements IMonomerLibFileManager {
|
|
|
31
32
|
private readonly fileValidator: MonomerLibFileValidator,
|
|
32
33
|
private readonly libHelper: IMonomerLibHelper,
|
|
33
34
|
public readonly eventManager: MonomerLibFileEventManager,
|
|
35
|
+
private readonly logger: ILogger,
|
|
34
36
|
) {
|
|
35
37
|
this.eventManager.updateValidLibraryFileListRequested$.subscribe(async () => {
|
|
36
38
|
await this.updateValidLibraryList();
|
|
@@ -46,19 +48,20 @@ export class MonomerLibFileManager implements IMonomerLibFileManager {
|
|
|
46
48
|
|
|
47
49
|
/** For internal use only, get {@link IMonomerLibHelper.getFileManager} */
|
|
48
50
|
public static async create(
|
|
49
|
-
libHelper: IMonomerLibHelper, eventManager: MonomerLibFileEventManager
|
|
51
|
+
libHelper: IMonomerLibHelper, eventManager: MonomerLibFileEventManager, logger: ILogger,
|
|
50
52
|
): Promise<MonomerLibFileManager> {
|
|
51
53
|
const helmSchemaRaw = await grok.dapi.files.readAsText(HELM_JSON_SCHEMA_PATH);
|
|
52
54
|
const helmSchema = JSON.parse(helmSchemaRaw) as JSONSchemaType<any>;
|
|
53
55
|
|
|
54
56
|
const fileValidator = new MonomerLibFileValidator(helmSchema);
|
|
55
|
-
return new MonomerLibFileManager(fileValidator, libHelper, eventManager);
|
|
57
|
+
return new MonomerLibFileManager(fileValidator, libHelper, eventManager, logger);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Add standard .json monomer library */
|
|
59
61
|
async addLibraryFile(fileContent: string, fileName: string): Promise<void> {
|
|
60
62
|
try {
|
|
61
|
-
|
|
63
|
+
const alreadyFileExists = await grok.dapi.files.exists(LIB_PATH + `${fileName}`);
|
|
64
|
+
if (alreadyFileExists) {
|
|
62
65
|
grok.shell.error(`File ${fileName} already exists`);
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
@@ -122,21 +125,17 @@ export class MonomerLibFileManager implements IMonomerLibFileManager {
|
|
|
122
125
|
return await this.eventManager.getValidLibraryPathsAsynchronously();
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
private async libraryFileExists(fileName: string): Promise<boolean> {
|
|
126
|
-
return await grok.dapi.files.exists(LIB_PATH + `${fileName}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
128
|
private async updateValidLibraryList(): Promise<void> {
|
|
130
129
|
const logPrefix: string = `${this.toLog()}.updateValidLibraryList()`;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
this.logger.debug(`${logPrefix}, start`);
|
|
131
|
+
this.filesPromise = this.filesPromise.then(async () => {
|
|
132
|
+
this.logger.debug(`${logPrefix}, IN`);
|
|
134
133
|
const invalidFiles = [] as string[];
|
|
135
134
|
// console.log(`files before validation:`, this.libraryEventManager.getValidFilesPathList());
|
|
136
135
|
const filePaths = await this.getFilePathsAtDefaultLocation();
|
|
137
136
|
|
|
138
137
|
if (!this.fileListHasChanged(filePaths)) {
|
|
139
|
-
|
|
138
|
+
this.logger.debug(`${logPrefix}, end, not changed`);
|
|
140
139
|
return;
|
|
141
140
|
}
|
|
142
141
|
|
|
@@ -160,18 +159,19 @@ export class MonomerLibFileManager implements IMonomerLibFileManager {
|
|
|
160
159
|
// console.log(`files after validation:`, this.libraryEventManager.getValidFilesPathList());
|
|
161
160
|
|
|
162
161
|
if (validLibraryPaths.some((el) => !el.endsWith('.json')))
|
|
163
|
-
|
|
162
|
+
this.logger.warning(`Wrong validation: ${validLibraryPaths}`);
|
|
164
163
|
|
|
165
164
|
if (invalidFiles.length > 0) {
|
|
166
165
|
const message = `Invalid monomer library files in ${LIB_PATH}` +
|
|
167
166
|
`, consider fixing or removing them: ${invalidFiles.join(', ')}`;
|
|
168
167
|
|
|
169
|
-
|
|
168
|
+
this.logger.warning(message);
|
|
170
169
|
// grok.shell.warning(message);
|
|
171
170
|
}
|
|
172
|
-
|
|
171
|
+
this.logger.debug(`${logPrefix}, OUT`);
|
|
173
172
|
});
|
|
174
|
-
|
|
173
|
+
this.logger.debug(`${logPrefix}, end`);
|
|
174
|
+
return this.filesPromise;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
private fileListHasChanged(newList: string[]): boolean {
|
|
@@ -191,21 +191,25 @@ export class MonomerLibFileManager implements IMonomerLibFileManager {
|
|
|
191
191
|
|
|
192
192
|
/** Get relative paths for files in LIB_PATH */
|
|
193
193
|
private async getFilePathsAtDefaultLocation(): Promise<string[]> {
|
|
194
|
+
const logPrefix = `${this.toLog()}.getFilePathsAtDefaultLocation()`;
|
|
195
|
+
this.logger.debug(`${logPrefix}, start`);
|
|
194
196
|
const list = await grok.dapi.files.list(LIB_PATH);
|
|
195
197
|
const paths = list.map((fileInfo) => {
|
|
196
198
|
return fileInfo.fullPath;
|
|
197
199
|
});
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// WARNING: an extra sanity check,
|
|
202
|
-
// caused by unexpected behavior of grok.dapi.files.list() when it returns non-existent paths
|
|
201
|
+
const checkForUi = false;
|
|
203
202
|
const existingPaths = [] as string[];
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
if (checkForUi) {
|
|
204
|
+
// WARNING: an extra sanity check,
|
|
205
|
+
// caused by unexpected behavior of grok.dapi.files.list() when it returns non-existent paths
|
|
206
|
+
for (const path of paths) {
|
|
207
|
+
const exists = await grok.dapi.files.exists(path);
|
|
208
|
+
if (exists)
|
|
209
|
+
existingPaths.push(path);
|
|
210
|
+
}
|
|
211
|
+
} else
|
|
212
|
+
existingPaths.push(...paths);
|
|
209
213
|
|
|
210
214
|
return existingPaths.map((path) => {
|
|
211
215
|
// Get relative path (to LIB_PATH)
|
|
@@ -14,7 +14,8 @@ export class MonomerLibFileValidator {
|
|
|
14
14
|
constructor(
|
|
15
15
|
private helmMonomerSchema: JSONSchemaType<any>
|
|
16
16
|
) {
|
|
17
|
-
|
|
17
|
+
// HELMMonomerSchema.json / #/properties/id uses a union type (string added by Maria Dolotova)
|
|
18
|
+
const ajv = new Ajv2020({allErrors: true, strictTuples: false, allowUnionTypes: true});
|
|
18
19
|
addErrors(ajv);
|
|
19
20
|
this.validateMonomerSchema = ajv.compile(this.helmMonomerSchema);
|
|
20
21
|
}
|
|
@@ -139,12 +139,9 @@ class LibraryControlsManager {
|
|
|
139
139
|
const logPrefix = `${this.toLog()}.createLibInput()`;
|
|
140
140
|
_package.logger.debug(`${logPrefix}, libFileName = '${libFileName}', start`);
|
|
141
141
|
const isMonomerLibrarySelected = !this.userLibSettings.exclude.includes(libFileName);
|
|
142
|
-
const libInput = ui.
|
|
143
|
-
libFileName,
|
|
144
|
-
|
|
145
|
-
(isSelected: boolean) => {
|
|
146
|
-
this.fileManager.eventManager.updateLibrarySelectionStatus(libFileName, isSelected);
|
|
147
|
-
});
|
|
142
|
+
const libInput = ui.input.bool(libFileName, {value: isMonomerLibrarySelected, onValueChanged: (input) => {
|
|
143
|
+
this.fileManager.eventManager.updateLibrarySelectionStatus(libFileName, input.value);
|
|
144
|
+
}});
|
|
148
145
|
ui.tooltip.bind(libInput.root, `Include monomers from ${libFileName}`);
|
|
149
146
|
const deleteIcon = ui.iconFA('trash-alt', () => this.promptForLibraryDeletion(libFileName));
|
|
150
147
|
ui.tooltip.bind(deleteIcon, `Delete ${libFileName}`);
|
|
@@ -38,18 +38,18 @@ export async function multipleSequenceAlignmentUI(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// UI for PepSea alignment
|
|
41
|
-
const methodInput = ui.
|
|
41
|
+
const methodInput = ui.input.choice('Method', {value: options.pepsea.method, items: pepseaMethods});
|
|
42
42
|
methodInput.setTooltip('Alignment method');
|
|
43
43
|
|
|
44
44
|
// UI for Kalign alignment
|
|
45
|
-
const terminalGapInput = ui.
|
|
45
|
+
const terminalGapInput = ui.input.float('Terminal gap', {value: options?.kalign?.terminalGap});
|
|
46
46
|
terminalGapInput.setTooltip('Penalty for opening a gap at the beginning or end of the sequence');
|
|
47
47
|
const kalignVersionDiv = ui.p(`Kalign version: ${kalignVersion}`, 'kalign-version');
|
|
48
48
|
|
|
49
49
|
// shared UI
|
|
50
|
-
const gapOpenInput = ui.
|
|
50
|
+
const gapOpenInput = ui.input.float('Gap open', {value: options.pepsea.gapOpen});
|
|
51
51
|
gapOpenInput.setTooltip('Gap opening penalty at group-to-group alignment');
|
|
52
|
-
const gapExtendInput = ui.
|
|
52
|
+
const gapExtendInput = ui.input.float('Gap extend', {value: options.pepsea.gapExtend});
|
|
53
53
|
gapExtendInput.setTooltip('Gap extension penalty to skip the alignment');
|
|
54
54
|
|
|
55
55
|
const msaParamsDiv = ui.inputs([gapOpenInput, gapExtendInput, terminalGapInput]);
|
|
@@ -69,25 +69,24 @@ export async function multipleSequenceAlignmentUI(
|
|
|
69
69
|
let performAlignment: (() => Promise<DG.Column<string> | null>) | undefined;
|
|
70
70
|
|
|
71
71
|
let prevSeqCol = seqCol;
|
|
72
|
-
const colInput = ui.
|
|
73
|
-
'Sequence', table, seqCol,
|
|
74
|
-
|
|
75
|
-
if (!valueCol || valueCol.semType !== DG.SEMTYPE.MACROMOLECULE) {
|
|
72
|
+
const colInput = ui.input.column(
|
|
73
|
+
'Sequence', {table: table, value: seqCol, onValueChanged: async (input: DG.InputBase<DG.Column<string>>): Promise<void> => {
|
|
74
|
+
if (!input.value || input.value.semType !== DG.SEMTYPE.MACROMOLECULE) {
|
|
76
75
|
okBtn.disabled = true;
|
|
77
76
|
await delay(0); // to
|
|
78
77
|
colInput.value = prevSeqCol as DG.Column<string>;
|
|
79
78
|
return;
|
|
80
79
|
}
|
|
81
|
-
prevSeqCol =
|
|
80
|
+
prevSeqCol = input.value;
|
|
82
81
|
okBtn.disabled = false;
|
|
83
82
|
performAlignment = await onColInputChange(
|
|
84
83
|
colInput.value, table, pepseaInputRootStyles, kalignInputRootStyles,
|
|
85
84
|
methodInput, clustersColInput, gapOpenInput, gapExtendInput, terminalGapInput,
|
|
86
85
|
);
|
|
87
|
-
},
|
|
86
|
+
}, filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE} as ColumnInputOptions
|
|
88
87
|
) as DG.InputBase<DG.Column<string>>;
|
|
89
88
|
colInput.setTooltip('Sequences column to use for alignment');
|
|
90
|
-
const clustersColInput = ui.
|
|
89
|
+
const clustersColInput = ui.input.column('Clusters', {table: table, value: options.clustersCol!});
|
|
91
90
|
clustersColInput.nullable = true;
|
|
92
91
|
|
|
93
92
|
const dlg = ui.dialog('MSA')
|
|
@@ -103,13 +103,13 @@ export async function runKalign(srcCol: DG.Column<string>, isAligned: boolean =
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// units
|
|
106
|
-
const srcUnits = srcCol.
|
|
106
|
+
const srcUnits = srcCol.meta.units;
|
|
107
107
|
//aligned
|
|
108
108
|
const tgtAligned = ALIGNMENT.SEQ_MSA;
|
|
109
109
|
//alphabet
|
|
110
110
|
const srcAlphabet = srcCol.getTag(bioTAGS.alphabet);
|
|
111
111
|
|
|
112
|
-
tgtCol.
|
|
112
|
+
tgtCol.meta.units = srcUnits;
|
|
113
113
|
tgtCol.setTag(bioTAGS.aligned, tgtAligned);
|
|
114
114
|
tgtCol.setTag(bioTAGS.alphabet, srcAlphabet);
|
|
115
115
|
tgtCol.semType = DG.SEMTYPE.MACROMOLECULE;
|
package/src/utils/pepsea.ts
CHANGED
|
@@ -91,7 +91,7 @@ export async function runPepsea(srcCol: DG.Column<string>, unUsedName: string,
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const alignedSequencesCol: DG.Column<string> = DG.Column.fromStrings(unUsedName, alignedSequences);
|
|
94
|
-
alignedSequencesCol.
|
|
94
|
+
alignedSequencesCol.meta.units = NOTATION.SEPARATOR;
|
|
95
95
|
alignedSequencesCol.setTag(bioTAGS.separator, C.PEPSEA.SEPARATOR);
|
|
96
96
|
alignedSequencesCol.setTag(bioTAGS.aligned, ALIGNMENT.SEQ_MSA);
|
|
97
97
|
alignedSequencesCol.setTag(bioTAGS.alphabet, ALPHABET.UN);
|
|
@@ -20,8 +20,8 @@ export function saveAsFastaUI() {
|
|
|
20
20
|
.find((gcol: DG.GridColumn) => gcol.name.toLowerCase().indexOf('id') !== -1);
|
|
21
21
|
const idDefaultValue = defaultIdGCol ? [defaultIdGCol.name] : [];
|
|
22
22
|
|
|
23
|
-
const idGColListInput = ui.
|
|
24
|
-
idGColList.map((gcol: DG.GridColumn) => gcol.name));
|
|
23
|
+
const idGColListInput = ui.input.multiChoice('Seq id columns', {value: idDefaultValue,
|
|
24
|
+
items: idGColList.map((gcol: DG.GridColumn) => gcol.name)});
|
|
25
25
|
|
|
26
26
|
const seqGColList: DG.GridColumn[] = wu.count(0).take(grid.columns.length)/* range rom 0 to grid.columns.length */
|
|
27
27
|
.map((colI: number) => grid.columns.byIndex(colI)!)
|
|
@@ -35,10 +35,10 @@ export function saveAsFastaUI() {
|
|
|
35
35
|
}).toArray();
|
|
36
36
|
|
|
37
37
|
const seqDefaultValue = seqGColList.length > 0 ? seqGColList[0].name : [];
|
|
38
|
-
const seqColInput = ui.
|
|
39
|
-
seqGColList.map((gCol: DG.GridColumn) => gCol.name));
|
|
38
|
+
const seqColInput = ui.input.choice('Seq column', {value: seqDefaultValue,
|
|
39
|
+
items: seqGColList.map((gCol: DG.GridColumn) => gCol.name)});
|
|
40
40
|
|
|
41
|
-
const lineWidthInput = ui.
|
|
41
|
+
const lineWidthInput = ui.input.int('FASTA line width', {value: FASTA_LINE_WIDTH});
|
|
42
42
|
|
|
43
43
|
ui.dialog({title: 'Save as FASTA'})
|
|
44
44
|
.add(ui.inputs([
|