sequenceserver 3.1.2 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/blast/tasks.rb +1 -1
  3. data/lib/sequenceserver/routes.rb +1 -1
  4. data/lib/sequenceserver/version.rb +1 -1
  5. data/lib/sequenceserver.rb +1 -1
  6. data/public/css/app.min.css +1 -1
  7. data/public/css/sequenceserver.css +0 -18
  8. data/public/css/sequenceserver.min.css +2 -2
  9. data/public/js/alignment_exporter.js +16 -28
  10. data/public/js/cloud_share_modal.js +42 -42
  11. data/public/js/form.js +12 -10
  12. data/public/js/grapher.js +4 -4
  13. data/public/js/hit.js +3 -3
  14. data/public/js/hits.js +276 -0
  15. data/public/js/jquery_world.js +1 -1
  16. data/public/js/mailto.js +1 -3
  17. data/public/js/null_plugins/report_plugins.js +1 -0
  18. data/public/js/options.js +2 -6
  19. data/public/js/query.js +1 -1
  20. data/public/js/report.js +68 -252
  21. data/public/js/report_root.js +7 -5
  22. data/public/js/search.js +28 -11
  23. data/public/js/sequence.js +158 -158
  24. data/public/js/sequence_modal.js +28 -36
  25. data/public/js/sidebar.js +7 -6
  26. data/public/js/tests/alignment_exporter.spec.js +38 -0
  27. data/public/js/tests/cloud_share_modal.spec.js +75 -0
  28. data/public/js/tests/report.spec.js +37 -15
  29. data/public/packages/jquery-ui@1.13.3.js +19070 -0
  30. data/public/sequenceserver-report.min.js +3 -2481
  31. data/public/sequenceserver-report.min.js.LICENSE.txt +300 -0
  32. data/public/sequenceserver-report.min.js.map +1 -0
  33. data/public/sequenceserver-search.min.js +3 -2382
  34. data/public/sequenceserver-search.min.js.LICENSE.txt +292 -0
  35. data/public/sequenceserver-search.min.js.map +1 -0
  36. data/views/layout.erb +3 -7
  37. data/views/search.erb +1 -1
  38. data/views/search_layout.erb +1 -1
  39. metadata +11 -5
  40. data/public/config.js +0 -147
  41. data/public/packages/jquery-ui@1.11.4.js +0 -16624
@@ -1,5 +1,6 @@
1
1
  import * as Exporter from './exporter';
2
2
  import _ from 'underscore';
3
+
3
4
  export default class AlignmentExporter {
4
5
  constructor() {
5
6
  this.prepare_alignments_for_export = this.prepare_alignments_for_export.bind(this);
@@ -7,49 +8,36 @@ export default class AlignmentExporter {
7
8
  }
8
9
 
9
10
  wrap_string(str, width) {
10
- var idx = 0;
11
- var wrapped = '';
12
- while(true) {
13
- wrapped += str.substring(idx, idx + width);
14
- idx += width;
15
- if(idx < str.length) {
16
- wrapped += '\n';
17
- } else {
18
- break;
19
- }
20
- }
21
- return wrapped;
11
+ return str.match(new RegExp(`.{1,${width}}`, 'g')).join('\n');
22
12
  }
23
13
 
24
14
  generate_fasta(hsps) {
15
+ let fasta = '';
25
16
 
26
- var fasta = '';
17
+ hsps.map(hsp => {
18
+ fasta += `>${hsp.query_id}:${hsp.qstart}-${hsp.qend}\n`;
19
+ fasta += `${hsp.qseq}\n`;
20
+ fasta += `>${hsp.query_id}:${hsp.qstart}-${hsp.qend}_alignment_${hsp.hit_id}:${hsp.sstart}-${hsp.send}\n`;
21
+ fasta += `${hsp.midline}\n`;
22
+ fasta += `>${hsp.hit_id}:${hsp.sstart}-${hsp.send}\n`;
23
+ fasta += `${hsp.sseq}\n`;
24
+ });
27
25
 
28
- _.each(hsps, _.bind(function (hsp) {
29
- fasta += '>'+hsp.query_id+':'+hsp.qstart+'-'+hsp.qend+'\n';
30
- fasta += hsp.qseq+'\n';
31
- fasta += '>'+hsp.query_id+':'+hsp.qstart+'-'+hsp.qend+'_alignment_'+hsp.hit_id+':'+hsp.sstart+'-'+hsp.send+'\n';
32
- fasta += hsp.midline+'\n';
33
- fasta += '>'+hsp.hit_id+':'+hsp.sstart+'-'+hsp.send+'\n';
34
- fasta += hsp.sseq+'\n';
35
- }, this));
36
26
  return fasta;
37
27
  }
38
28
 
39
29
  get_alignments_download_metadata(hsps, filename_prefix){
40
- var fasta = this.generate_fasta(hsps);
41
- var blob = new Blob([fasta], { type: 'text/fasta' });
42
- var filename = Exporter.sanitize_filename(filename_prefix) + '.txt';
30
+ const fasta = this.generate_fasta(hsps);
31
+ const blob = new Blob([fasta], { type: 'text/fasta' });
32
+ const filename = Exporter.sanitize_filename(filename_prefix) + '.txt';
43
33
  return {filename, blob};
44
34
  }
45
35
 
46
-
47
36
  prepare_alignments_for_export(hsps, filename_prefix) {
48
37
  const { filename, blob } = this.get_alignments_download_metadata(hsps, filename_prefix);
49
- const blob_url = Exporter.generate_blob_url(blob, filename);
50
- return blob_url;
38
+ return Exporter.generate_blob_url(blob, filename);
51
39
  }
52
-
40
+
53
41
  export_alignments(hsps, filename_prefix) {
54
42
  const { filename, blob } = this.get_alignments_download_metadata(hsps, filename_prefix);
55
43
  Exporter.download_blob(blob, filename);
@@ -28,13 +28,11 @@ export default class CloudShareModal extends React.Component {
28
28
  this.setState({ [name]: inputValue });
29
29
  }
30
30
 
31
- handleSubmit = (e) => {
31
+ handleSubmit = async (e) => {
32
32
  e.preventDefault();
33
33
 
34
34
  const { email } = this.state;
35
- const regex = /\/([^/]+)(?:\/|#|\?|$)/;
36
- const match = window.location.pathname.match(regex);
37
- const jobId = match[1];
35
+ const jobId = this.getJobIdFromPath();
38
36
 
39
37
  this.setState({ formState: 'loading' });
40
38
 
@@ -43,33 +41,40 @@ export default class CloudShareModal extends React.Component {
43
41
  sender_email: email
44
42
  };
45
43
 
46
- fetch('/cloud_share', {
47
- method: 'POST',
48
- headers: {
49
- 'Content-Type': 'application/json'
50
- },
51
- body: JSON.stringify(requestData)
52
- })
53
- .then(response => response.json())
54
- .then(data => {
55
- if (data.shareable_url) {
56
- // Successful response
57
- this.setState({ formState: 'results', shareableurl: data.shareable_url });
58
- } else if (data.errors) {
59
- // Error response with specific error messages
60
- const errorMessages = data.errors;
61
- this.setState({ formState: 'error', errorMessages });
62
- } else {
63
- // Generic error message
64
- throw new Error('Unknown error submitting form');
65
- }
66
- })
67
- .catch(error => {
68
- this.setState({
69
- formState: 'error',
70
- errorMessages: [error.message]
71
- });
44
+ try {
45
+ const response = await fetch('/cloud_share', {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json'
49
+ },
50
+ body: JSON.stringify(requestData)
72
51
  });
52
+
53
+ if (!response.ok) {
54
+ throw new Error('Network response was not ok');
55
+ }
56
+
57
+ const data = await response.json();
58
+
59
+ if (data.shareable_url) {
60
+ this.setState({ formState: 'results', shareableurl: data.shareable_url });
61
+ } else if (data.errors) {
62
+ this.setState({ formState: 'error', errorMessages: data.errors });
63
+ } else {
64
+ throw new Error('Unknown error submitting form');
65
+ }
66
+ } catch (error) {
67
+ this.setState({
68
+ formState: 'error',
69
+ errorMessages: [error.message]
70
+ });
71
+ }
72
+ }
73
+
74
+ getJobIdFromPath = () => {
75
+ const regex = /\/([^/]+)(?:\/|#|\?|$)/;
76
+ const match = window.location.pathname.match(regex);
77
+ return match ? match[1] : match;
73
78
  }
74
79
 
75
80
  renderLoading() {
@@ -93,18 +98,13 @@ export default class CloudShareModal extends React.Component {
93
98
  return (
94
99
  <>
95
100
  {
96
- _.map(
97
- errorMessages,
98
- _.bind(function (errorMessage) {
99
- return (
100
- <div className="fastan">
101
- <div className="section-content">
102
- <div className="modal-error">{errorMessage}</div>
103
- </div>
104
- </div>
105
- );
106
- }, this)
107
- )
101
+ errorMessages.map((errorMessage, index) => (
102
+ <div key={`fastan-${index}`} className="fastan">
103
+ <div className="section-content">
104
+ <div className="modal-error">{errorMessage}</div>
105
+ </div>
106
+ </div>
107
+ ))
108
108
  }
109
109
  {this.renderForm()}
110
110
  </>
data/public/js/form.js CHANGED
@@ -34,6 +34,8 @@ export class Form extends Component {
34
34
  this.handleAlgoChanged = this.handleAlgoChanged.bind(this);
35
35
  this.handleFormSubmission = this.handleFormSubmission.bind(this);
36
36
  this.formRef = createRef();
37
+ this.query = createRef();
38
+ this.button = createRef();
37
39
  this.setButtonState = this.setButtonState.bind(this);
38
40
  }
39
41
 
@@ -65,7 +67,7 @@ export class Form extends Component {
65
67
  * (if any).
66
68
  */
67
69
  if (data['query']) {
68
- this.refs.query.value(data['query']);
70
+ this.query.current.value(data['query']);
69
71
  }
70
72
 
71
73
  setTimeout(function () {
@@ -96,7 +98,7 @@ export class Form extends Component {
96
98
  evt.preventDefault();
97
99
  const form = this.formRef.current;
98
100
  const formData = new FormData(form);
99
- formData.append('method', this.refs.button.state.methods[0]);
101
+ formData.append('method', this.button.current.state.methods[0]);
100
102
  fetch(window.location.href, {
101
103
  method: 'POST',
102
104
  body: formData
@@ -118,7 +120,7 @@ export class Form extends Component {
118
120
  var database_type = this.databaseType;
119
121
  var sequence_type = this.sequenceType;
120
122
 
121
- if (this.refs.query.isEmpty()) {
123
+ if (this.query.current.isEmpty()) {
122
124
  return [];
123
125
  }
124
126
 
@@ -165,8 +167,8 @@ export class Form extends Component {
165
167
  }
166
168
 
167
169
  setButtonState() {
168
- this.refs.button.setState({
169
- hasQuery: !this.refs.query.isEmpty(),
170
+ this.button.current.setState({
171
+ hasQuery: !this.query.current.isEmpty(),
170
172
  hasDatabases: !!this.databaseType,
171
173
  methods: this.determineBlastMethods()
172
174
  });
@@ -205,16 +207,16 @@ export class Form extends Component {
205
207
  <form id="blast" ref={this.formRef} onSubmit={this.handleFormSubmission}>
206
208
  <input type="hidden" name="_csrf" value={document.querySelector('meta[name="_csrf"]').content} />
207
209
  <div className="px-4">
208
- <SearchQueryWidget ref="query" onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged}/>
210
+ <SearchQueryWidget ref={this.query} onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged}/>
209
211
 
210
212
  {this.useTreeWidget() ?
211
- <DatabasesTree ref="databases"
213
+ <DatabasesTree
212
214
  databases={this.state.databases} tree={this.state.tree}
213
215
  preSelectedDbs={this.state.preSelectedDbs}
214
216
  onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
215
217
  onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
216
218
  :
217
- <Databases ref="databases" databases={this.state.databases}
219
+ <Databases databases={this.state.databases}
218
220
  preSelectedDbs={this.state.preSelectedDbs}
219
221
  onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
220
222
  onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
@@ -234,7 +236,7 @@ export class Form extends Component {
234
236
  <label className="block my-4 md:my-2">
235
237
  <input type="checkbox" id="toggleNewTab" /> Open results in new tab
236
238
  </label>
237
- <SearchButton ref="button" onAlgoChanged={this.handleAlgoChanged} />
239
+ <SearchButton ref={this.button} onAlgoChanged={this.handleAlgoChanged} />
238
240
  </div>
239
241
 
240
242
  </form>
@@ -305,4 +307,4 @@ class MixedNotification extends Component {
305
307
  </div>
306
308
  );
307
309
  }
308
- }
310
+ }
data/public/js/grapher.js CHANGED
@@ -17,7 +17,7 @@ export default function Grapher(Graph) {
17
17
  return class extends React.Component {
18
18
  constructor(props) {
19
19
  super(props);
20
- this.name = Graph.name();
20
+ this.name = Graph.name(this.props);
21
21
  this.collapsePreferences = new CollapsePreferences(this);
22
22
  let isCollapsed = this.collapsePreferences.preferenceStoredAsCollapsed();
23
23
  this.state = { collapsed: Graph.canCollapse() && (this.props.collapsed || isCollapsed) };
@@ -30,12 +30,12 @@ export default function Grapher(Graph) {
30
30
 
31
31
  render() {
32
32
  // Do not render when Graph.name() is null
33
- if (Graph.name() === null) {
33
+ if (Graph.name(this.props) === null) {
34
34
  return null;
35
35
  } else {
36
36
  var cssClasses = Graph.className() + ' grapher';
37
37
  return (
38
- <div ref="grapher" className={cssClasses}>
38
+ <div className={cssClasses}>
39
39
  {this.header()}
40
40
  {this.svgContainerJSX()}
41
41
  </div>
@@ -51,7 +51,7 @@ export default function Grapher(Graph) {
51
51
  onClick={() => this.collapsePreferences.toggleCollapse()}
52
52
  >
53
53
  {this.collapsePreferences.renderCollapseIcon()}
54
- &nbsp;{Graph.name()}
54
+ &nbsp;{Graph.name(this.props)}
55
55
  </h4>
56
56
  {!this.state.collapsed && this.graphLinksJSX()}
57
57
  </div>;
data/public/js/hit.js CHANGED
@@ -80,14 +80,14 @@ export default class extends Component {
80
80
  return `get_sequence/?sequence_ids=${sequenceIDs}&database_ids=${databaseIDs}`;
81
81
  }
82
82
 
83
- downloadFASTA(event) {
83
+ downloadFASTA(_event) {
84
84
  var sequenceIDs = [this.sequenceID()];
85
85
  downloadFASTA(sequenceIDs, this.databaseIDs());
86
86
  }
87
87
 
88
88
  // Event-handler for exporting alignments.
89
89
  // Calls relevant method on AlignmentExporter defined in alignment_exporter.js.
90
- downloadAlignment(event) {
90
+ downloadAlignment(_event) {
91
91
  var hsps = _.map(this.props.hit.hsps, _.bind(function (hsp) {
92
92
  hsp.query_id = this.props.query.id;
93
93
  hsp.hit_id = this.props.hit.id;
@@ -246,4 +246,4 @@ export default class extends Component {
246
246
  </div>
247
247
  );
248
248
  }
249
- }
249
+ }
data/public/js/hits.js ADDED
@@ -0,0 +1,276 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { Component } from 'react';
3
+ import _ from 'underscore';
4
+
5
+ import { ReportQuery } from './query';
6
+ import Hit from './hit';
7
+ import HSP from './hsp';
8
+ import AlignmentExporter from './alignment_exporter';
9
+ /* eslint-enable no-unused-vars */
10
+
11
+ class Hits extends Component {
12
+ constructor(props) {
13
+ super(props);
14
+ this.numUpdates = 0;
15
+ this.nextQuery = 0;
16
+ this.nextHit = 0;
17
+ this.nextHSP = 0;
18
+ this.maxHSPs = 3; // max HSPs to render in a cycle
19
+ this.state = props.state;
20
+ this.prepareAlignmentOfSelectedHits = this.prepareAlignmentOfSelectedHits.bind(this);
21
+ }
22
+
23
+ componentDidMount() {
24
+ this.componentDidUpdate(this.props, this.state);
25
+ }
26
+
27
+ /**
28
+ * Called for the first time after as BLAST results have been retrieved from
29
+ * the server and added to this.state by fetchResults. Only summary overview
30
+ * and circos would have been rendered at this point. At this stage we kick
31
+ * start iteratively adding 1 HSP to the page every 25 milli-seconds.
32
+ */
33
+ componentDidUpdate(prevProps, prevState) {
34
+ // Log to console how long the last update take?
35
+ // console.log((Date.now() - this.lastTimeStamp) / 1000);
36
+
37
+ // Lock sidebar in its position on the first update.
38
+ if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
39
+ this.affixSidebar();
40
+ }
41
+
42
+ // Queue next update if we have not rendered all results yet.
43
+ if (this.nextQuery < this.state.queries.length) {
44
+ // setTimeout is used to clear call stack and space out
45
+ // the updates giving the browser a chance to respond
46
+ // to user interactions.
47
+ setTimeout(() => this.updateState(), 25);
48
+ } else {
49
+ this.props.componentFinishedUpdating();
50
+ }
51
+
52
+ this.props.plugins.componentDidUpdate(prevProps, prevState);
53
+ }
54
+
55
+ /* eslint complexity: ["error", 6] */
56
+ /* ---------------------
57
+ * Push next slice of results to React for rendering.
58
+ */
59
+ updateState() {
60
+ var results = { items: [], numHSPsProcessed: 0 };
61
+ this.processQueries(results);
62
+
63
+ // Push the components to react for rendering.
64
+ this.numUpdates++;
65
+ this.lastTimeStamp = Date.now();
66
+ this.setState({
67
+ results: this.state.results.concat(results.items),
68
+ veryBig: this.numUpdates >= 250,
69
+ });
70
+ }
71
+
72
+ processQueries(results) {
73
+ while (this.nextQuery < this.state.queries.length) {
74
+ var query = this.state.queries[this.nextQuery];
75
+
76
+ // We may see a query multiple times during rendering because only
77
+ // 3 hsps are rendered in each cycle, but we want to create the
78
+ // corresponding Query component only the first time we see it.
79
+ if (this.nextHit == 0 && this.nextHSP == 0) {
80
+ results.items.push(this.renderReportQuery(query));
81
+ results.items.push(...this.props.plugins.queryResults(query));
82
+ }
83
+
84
+ this.processHits(results, query);
85
+ this.itterateLoops(['nextQuery', 'nextHit'], query.hits.length);
86
+ if (results.numHSPsProcessed == this.maxHSPs) break;
87
+ }
88
+ }
89
+
90
+ processHits(results, query) {
91
+ while (this.nextHit < query.hits.length) {
92
+ var hit = query.hits[this.nextHit];
93
+ // We may see a hit multiple times during rendering because only
94
+ // 10 hsps are rendered in each cycle, but we want to create the
95
+ // corresponding Hit component only the first time we see it.
96
+ if (this.nextHSP == 0) results.items.push(this.renderHit(query, hit));
97
+
98
+ this.processHSPS(results, query, hit);
99
+ this.itterateLoops(['nextHit', 'nextHSP'], hit.hsps.length);
100
+ if (results.numHSPsProcessed == this.maxHSPs) break;
101
+ }
102
+ }
103
+
104
+ processHSPS(results, query, hit) {
105
+ while (this.nextHSP < hit.hsps.length) {
106
+ // Get nextHSP and increment the counter.
107
+ var hsp = hit.hsps[this.nextHSP++];
108
+ results.items.push(
109
+ this.renderHsp(query, hit, hsp)
110
+ );
111
+ results.numHSPsProcessed++;
112
+ if (results.numHSPsProcessed == this.maxHSPs) break;
113
+ }
114
+ }
115
+
116
+ /*
117
+ * this function check if 2nd argument is reach end of it
118
+ */
119
+ itterateLoops(args, length) {
120
+ if (this[args[1]] != length) return;
121
+
122
+ this[args[0]]++;
123
+ this[args[1]] = 0;
124
+ }
125
+
126
+ renderHsp(query, hit, hsp) {
127
+ return (
128
+ <HSP
129
+ key={
130
+ 'Query_' +
131
+ query.number +
132
+ '_Hit_' +
133
+ hit.number +
134
+ '_HSP_' +
135
+ hsp.number
136
+ }
137
+ query={query}
138
+ hit={hit}
139
+ hsp={hsp}
140
+ algorithm={this.state.program}
141
+ showHSPNumbers={hit.hsps.length > 1}
142
+ {...this.props}
143
+ />
144
+ );
145
+ }
146
+
147
+ renderHit(query, hit) {
148
+ return (
149
+ <Hit
150
+ key={'Query_' + query.number + '_Hit_' + hit.number}
151
+ query={query}
152
+ hit={hit}
153
+ algorithm={this.state.program}
154
+ querydb={this.state.querydb}
155
+ selectHit={this.selectHit}
156
+ imported_xml={this.state.imported_xml}
157
+ non_parse_seqids={this.state.non_parse_seqids}
158
+ showQueryCrumbs={this.state.queries.length > 1}
159
+ showHitCrumbs={query.hits.length > 1}
160
+ veryBig={this.state.veryBig}
161
+ onChange={this.prepareAlignmentOfSelectedHits}
162
+ {...this.props}
163
+ />
164
+ );
165
+ }
166
+
167
+ renderReportQuery(query) {
168
+ return (
169
+ <ReportQuery
170
+ key={'Query_' + query.id}
171
+ query={query}
172
+ program={this.state.program}
173
+ querydb={this.state.querydb}
174
+ showQueryCrumbs={this.state.queries.length > 1}
175
+ non_parse_seqids={this.state.non_parse_seqids}
176
+ imported_xml={this.state.imported_xml}
177
+ veryBig={this.state.veryBig}
178
+ />
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Affixes the sidebar.
184
+ */
185
+ affixSidebar() {
186
+ var $sidebar = $('.sidebar');
187
+ var sidebarOffset = $sidebar.offset();
188
+ if (sidebarOffset) {
189
+ $sidebar.affix({
190
+ offset: {
191
+ top: sidebarOffset.top,
192
+ },
193
+ });
194
+ }
195
+ }
196
+
197
+ /* eslint complexity: ["error", 6] */
198
+ /* -----------------------------------
199
+ * Event-handler when hit is selected
200
+ * Adds glow to hit component.
201
+ * Updates number of Fasta that can be downloaded
202
+ */
203
+ selectHit(id) {
204
+ var checkbox = $('#' + id);
205
+ var num_checked = $('.hit-links :checkbox:checked').length;
206
+
207
+ if (!checkbox || !checkbox.val()) return;
208
+
209
+ var $hit = $(checkbox.data('target'));
210
+
211
+ // Highlight selected hit and enable 'Download FASTA/Alignment of
212
+ // selected' links.
213
+ if (checkbox.is(':checked')) {
214
+ $hit.addClass('glow');
215
+ $hit.next('.hsp').addClass('glow');
216
+ $('.download-fasta-of-selected').enable();
217
+ $('.download-alignment-of-selected').enable();
218
+ } else {
219
+ $hit.removeClass('glow');
220
+ $hit.next('.hsp').removeClass('glow');
221
+ $('.download-fasta-of-selected').attr('href', '#').removeAttr('download');
222
+ }
223
+
224
+ var $a = $('.download-fasta-of-selected');
225
+ var $b = $('.download-alignment-of-selected');
226
+
227
+ if (num_checked >= 1) {
228
+ $a.find('.text-bold').html(num_checked);
229
+ $b.find('.text-bold').html(num_checked);
230
+ }
231
+
232
+ if (num_checked == 0) {
233
+ $a.addClass('disabled').find('.text-bold').html('');
234
+ $b.addClass('disabled').find('.text-bold').html('');
235
+ }
236
+ }
237
+
238
+ prepareAlignmentOfSelectedHits() {
239
+ var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
240
+ return this.value;
241
+ }).get();
242
+
243
+ if(!sequence_ids.length){
244
+ // remove attributes from link if sequence_ids array is empty
245
+ $('.download-alignment-of-selected').attr('href', '#').removeAttr('download');
246
+ return;
247
+
248
+ }
249
+ if(this.state.alignment_blob_url){
250
+ // always revoke existing url if any because this method will always create a new url
251
+ window.URL.revokeObjectURL(this.state.alignment_blob_url);
252
+ }
253
+ var hsps_arr = [];
254
+ var aln_exporter = new AlignmentExporter();
255
+ const self = this;
256
+ _.each(this.state.queries, _.bind(function (query) {
257
+ _.each(query.hits, function (hit) {
258
+ if (_.indexOf(sequence_ids, hit.id) != -1) {
259
+ hsps_arr = hsps_arr.concat(self.props.populate_hsp_array(hit, query.id));
260
+ }
261
+ });
262
+ }, this));
263
+ const filename = 'alignment-' + sequence_ids.length + '_hits.txt';
264
+ const blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename);
265
+ // set required download attributes for link
266
+ $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename);
267
+ // track new url for future removal
268
+ this.setState({alignment_blob_url: blob_url});
269
+ }
270
+
271
+ render() {
272
+ return this.state.results;
273
+ }
274
+ }
275
+
276
+ export default Hits;
@@ -1,5 +1,5 @@
1
1
  import $ from 'jquery';
2
- import '../packages/jquery-ui@1.11.4';
2
+ import '../packages/jquery-ui@1.13.3';
3
3
  import 'bootstrap';
4
4
 
5
5
  global.$ = $;
data/public/js/mailto.js CHANGED
@@ -5,9 +5,7 @@ export default function asMailtoHref(querydb, program, numQueries, url, isOpenAc
5
5
  }
6
6
 
7
7
  function formatDatabases(querydb) {
8
- return querydb
9
- .slice(0, 15)
10
- .map(db => ' ' + db.title);
8
+ return querydb ? querydb.slice(0, 15).map(db => ' ' + db.title) : "";
11
9
  }
12
10
 
13
11
  function composeEmail(dbsArr, program, numQueries, url, isOpenAccess) {
@@ -1,4 +1,5 @@
1
1
  import Histogram from 'histogram';
2
+ import chroma from 'chroma-js';
2
3
 
3
4
  class ReportPlugins {
4
5
  constructor(parent) {
data/public/js/options.js CHANGED
@@ -110,18 +110,14 @@ export class Options extends Component {
110
110
  }
111
111
 
112
112
  advancedParamsJSX() {
113
- if (this.state.paramsMode !== 'advanced') {
114
- return null;
115
- }
116
-
117
- let classNames = 'flex-grow block px-4 py-1 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 text-base';
113
+ let classNames = 'flex-grow block px-4 py-1 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 text-base font-mono';
118
114
 
119
115
  if (this.state.textValue) {
120
116
  classNames += ' bg-yellow-100';
121
117
  }
122
118
 
123
119
  return(
124
- <div className="w-full">
120
+ <div className={this.state.paramsMode !== 'advanced' ? 'w-full hidden' : 'w-full'}>
125
121
  <div className="flex items-end">
126
122
  <label className="flex items-center" htmlFor="advanced">
127
123
  Advanced parameters
data/public/js/query.js CHANGED
@@ -348,7 +348,7 @@ export class SearchQueryWidget extends Component {
348
348
  className="sequence">
349
349
  <textarea
350
350
  id="sequence" ref={this.textareaRef}
351
- className="block w-full p-4 text-gray-900 border border-gray-300 rounded-l-lg rounded-tr-lg bg-gray-50 text-base text-monospace"
351
+ className="block w-full p-4 text-gray-900 border border-gray-300 rounded-l-lg rounded-tr-lg bg-gray-50 text-base font-mono"
352
352
  name="sequence" value={this.state.value}
353
353
  rows="6"
354
354
  required="required"