Skip to content

Commit 680f48d

Browse files
authored
Add modal gate/explanation wrapper for all Clustal Omega buttons (#1505)
* Add modal wrapper for all Clustal Omega buttons * add configurable warn and block thresholds that can optionally read from form values * use theme color for button * improve contrast in button text, change limits * disable gene page submit button when too few sequences * Add title guidance when submit button is disabled * reorder modal content and use Banner for warning and block * new paragraph break
1 parent 73c2c10 commit 680f48d

File tree

6 files changed

+221
-17
lines changed

6 files changed

+221
-17
lines changed

packages/libs/web-common/src/components/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { default as TabularReporterFormSubmitButtons } from './reporters/Tabular
1111
export { SiteSearchInput } from '../components/SiteSearch/SiteSearchInput';
1212
export { default as DownloadLink } from '../App/Studies/DownloadLink';
1313
export { SearchCheckboxTree } from '../components/homepage/SearchPane';
14+
export { default as ClustalAlignmentForm } from './records/ClustalAlignmentForm';
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import React, { useRef, useState, FormEvent } from 'react';
2+
import { Dialog } from '@veupathdb/wdk-client/lib/Components';
3+
import Banner from '@veupathdb/coreui/lib/components/banners/Banner';
4+
5+
interface ClustalAlignmentFormProps {
6+
action: string;
7+
sequenceCount: number;
8+
children: React.ReactNode;
9+
sequenceType?: string;
10+
warnThreshold?: number | ((form: HTMLFormElement) => number);
11+
blockThreshold?: number | ((form: HTMLFormElement) => number);
12+
}
13+
14+
const DEFAULT_WARN_THRESHOLD = 50;
15+
const DEFAULT_BLOCK_THRESHOLD = 1000;
16+
17+
export default function ClustalAlignmentForm({
18+
action,
19+
sequenceCount,
20+
children,
21+
sequenceType = 'sequences',
22+
warnThreshold,
23+
blockThreshold,
24+
}: ClustalAlignmentFormProps) {
25+
const [showModal, setShowModal] = useState(false);
26+
const [evaluatedWarnThreshold, setEvaluatedWarnThreshold] =
27+
useState<number | null>(null);
28+
const [evaluatedBlockThreshold, setEvaluatedBlockThreshold] =
29+
useState<number | null>(null);
30+
const formRef = useRef<HTMLFormElement>(null);
31+
32+
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
33+
event.preventDefault();
34+
35+
// Evaluate thresholds dynamically if they're functions
36+
const actualWarn =
37+
typeof warnThreshold === 'function'
38+
? formRef.current
39+
? warnThreshold(formRef.current)
40+
: DEFAULT_WARN_THRESHOLD
41+
: warnThreshold ?? DEFAULT_WARN_THRESHOLD;
42+
const actualBlock =
43+
typeof blockThreshold === 'function'
44+
? formRef.current
45+
? blockThreshold(formRef.current)
46+
: DEFAULT_BLOCK_THRESHOLD
47+
: blockThreshold ?? DEFAULT_BLOCK_THRESHOLD;
48+
49+
setEvaluatedWarnThreshold(actualWarn);
50+
setEvaluatedBlockThreshold(actualBlock);
51+
setShowModal(true);
52+
};
53+
54+
const handleConfirm = () => {
55+
setShowModal(false);
56+
if (formRef.current) {
57+
formRef.current.submit();
58+
}
59+
};
60+
61+
const handleCancel = () => {
62+
setShowModal(false);
63+
};
64+
65+
const isBlocked =
66+
evaluatedBlockThreshold !== null && sequenceCount > evaluatedBlockThreshold;
67+
const showWarning =
68+
evaluatedWarnThreshold !== null &&
69+
evaluatedBlockThreshold !== null &&
70+
sequenceCount > evaluatedWarnThreshold &&
71+
sequenceCount <= evaluatedBlockThreshold;
72+
73+
return (
74+
<>
75+
<form
76+
ref={formRef}
77+
action={action}
78+
target="_blank"
79+
method="post"
80+
onSubmit={handleSubmit}
81+
>
82+
{children}
83+
</form>
84+
85+
<Dialog
86+
open={showModal}
87+
modal
88+
title="Run Clustal Omega Alignment"
89+
onClose={handleCancel}
90+
>
91+
<div style={{ padding: '10px', width: '500px' }}>
92+
{showWarning && (
93+
<Banner
94+
banner={{
95+
type: 'warning',
96+
message: (
97+
<>
98+
You have selected{' '}
99+
<strong>
100+
{sequenceCount} {sequenceType}
101+
</strong>
102+
. Aligning this many {sequenceType} may take several minutes
103+
to complete.
104+
</>
105+
),
106+
}}
107+
/>
108+
)}
109+
{isBlocked && (
110+
<Banner
111+
banner={{
112+
type: 'error',
113+
message: (
114+
<>
115+
You have selected{' '}
116+
<strong>
117+
{sequenceCount} {sequenceType}
118+
</strong>
119+
, which exceeds the maximum limit.
120+
<br />
121+
Please reduce your selection to fewer than{' '}
122+
{evaluatedBlockThreshold} {sequenceType} to proceed.
123+
</>
124+
),
125+
}}
126+
/>
127+
)}
128+
{!isBlocked && (
129+
<>
130+
<p style={{ marginTop: '0px' }}>
131+
The alignment results will open in a new browser tab.
132+
</p>
133+
<p>The tab may appear empty while the alignment is running.</p>
134+
<p>
135+
<strong>
136+
Please be patient and avoid resubmitting - multiple requests
137+
will not make it faster and can overload our servers.
138+
</strong>
139+
</p>
140+
</>
141+
)}
142+
<div
143+
style={{
144+
marginTop: '20px',
145+
display: 'flex',
146+
gap: '10px',
147+
justifyContent: 'flex-end',
148+
}}
149+
>
150+
<button type="button" className="btn" onClick={handleCancel}>
151+
{isBlocked ? 'OK' : 'Cancel'}
152+
</button>
153+
{!isBlocked && (
154+
<button
155+
type="button"
156+
className="btn"
157+
onClick={handleConfirm}
158+
style={{
159+
backgroundColor: 'var(--coreui-color-primary)',
160+
color: 'white',
161+
fontWeight: 600,
162+
}}
163+
>
164+
Continue Alignment
165+
</button>
166+
)}
167+
</div>
168+
</div>
169+
</Dialog>
170+
</>
171+
);
172+
}

packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/controllers/PopsetResultSummaryViewTableController.jsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import React from 'react';
22
import { projectId } from '../../config';
33
import { ResultTableSummaryViewPlugin } from '@veupathdb/wdk-client/lib/Plugins';
4+
import { ClustalAlignmentForm } from '@veupathdb/web-common/lib/components';
45

5-
const title = `Please select at least two isolates to run Clustal Omega. Note: only isolates from a single page will be aligned.
6+
const buttonHelp = `Please select at least two isolates to run Clustal Omega. Note: only isolates from a single page will be aligned.
67
The result is an alignment of the locus that was used to type the isolates.
78
(Increase the 'Rows per page' to increase the number that can be aligned).`;
89

910
export default ResultTableSummaryViewPlugin.withOptions({
1011
tableActions: [
1112
{
1213
element: (selectedRecords) => (
13-
<form action="/cgi-bin/isolateAlignment" target="_blank" method="post">
14+
<ClustalAlignmentForm
15+
action="/cgi-bin/isolateAlignment"
16+
sequenceCount={selectedRecords.length}
17+
sequenceType="isolates"
18+
>
1419
<input type="hidden" name="project_id" value={projectId} />
1520
<input type="hidden" name="type" />
1621
<input type="hidden" name="sid" />
@@ -26,11 +31,15 @@ export default ResultTableSummaryViewPlugin.withOptions({
2631
<button
2732
className="btn"
2833
disabled={selectedRecords.length < 2}
29-
title={title}
34+
title={
35+
selectedRecords.length < 2
36+
? buttonHelp
37+
: 'Run Clustal Omega alignment'
38+
}
3039
>
3140
Run Clustal Omega
3241
</button>
33-
</form>
42+
</ClustalAlignmentForm>
3443
),
3544
},
3645
],

packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { preorderSeq } from '@veupathdb/wdk-client/lib/Utils/TreeUtils';
2828

2929
import DatasetGraph from '@veupathdb/web-common/lib/components/DatasetGraph';
3030
import { EdaDatasetGraph } from '@veupathdb/web-common/lib/components/EdaDatasetGraph';
31+
import { ClustalAlignmentForm } from '@veupathdb/web-common/lib/components';
3132
import { ExternalResourceContainer } from '@veupathdb/web-common/lib/components/ExternalResource';
3233
import Sequence from '@veupathdb/web-common/lib/components/records/Sequence';
3334
import { isNodeOverflowing } from '@veupathdb/web-common/lib/util/domUtils';
@@ -1603,8 +1604,22 @@ class OrthologsForm extends SortKeyTable {
16031604
/>
16041605
);
16051606
} else {
1607+
// TODO: Discuss how to retain "large flanking region" warning in the modal
1608+
// Original message: "Please note: selecting a large flanking region or a large number of sequences will take several minutes to align."
16061609
return (
1607-
<form action="/cgi-bin/isolateAlignment" target="_blank" method="post">
1610+
<ClustalAlignmentForm
1611+
action="/cgi-bin/isolateAlignment"
1612+
sequenceCount={this.state.selectedRowIds.length + 1}
1613+
sequenceType="genes"
1614+
warnThreshold={(form) => {
1615+
const formData = new FormData(form);
1616+
return formData.get('sequence_Type') === 'genomic' ? 10 : 1000;
1617+
}}
1618+
blockThreshold={(form) => {
1619+
const formData = new FormData(form);
1620+
return formData.get('sequence_Type') === 'genomic' ? 50 : 1000;
1621+
}}
1622+
>
16081623
<input type="hidden" name="type" value="geneOrthologs" />
16091624
<input type="hidden" name="project_id" value={projectId} />
16101625
<input type="hidden" name="gene_ids" value={source_id} />
@@ -1628,10 +1643,6 @@ class OrthologsForm extends SortKeyTable {
16281643
alignment:
16291644
</b>
16301645
</p>
1631-
<p>
1632-
Please note: selecting a large flanking region or a large number of
1633-
sequences will take several minutes to align.
1634-
</p>
16351646
<div id="userOptions">
16361647
{is_protein && (
16371648
<>
@@ -1693,9 +1704,18 @@ class OrthologsForm extends SortKeyTable {
16931704
<option value="vie">VIENNA</option>
16941705
</select>
16951706
</p>
1696-
<input type="submit" value="Run Clustal Omega for selected genes" />
1707+
<input
1708+
type="submit"
1709+
value="Run Clustal Omega for selected genes"
1710+
disabled={this.state.selectedRowIds.length < 2}
1711+
title={
1712+
this.state.selectedRowIds.length < 2
1713+
? 'Check two or more checkboxes in the table above to use this feature.'
1714+
: ''
1715+
}
1716+
/>
16971717
</div>
1698-
</form>
1718+
</ClustalAlignmentForm>
16991719
);
17001720
}
17011721
}

packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { RecordTableProps, WrappedComponentProps } from './Types';
1010
import { useOrthoService } from 'ortho-client/hooks/orthoService';
1111
import { Loading, Link } from '@veupathdb/wdk-client/lib/Components';
1212
import { Branch, parseNewick } from 'patristic';
13+
import { ClustalAlignmentForm } from '@veupathdb/web-common/lib/components';
1314
import {
1415
AttributeValue,
1516
TableValue,
@@ -799,15 +800,15 @@ export function RecordTable_Sequences(
799800
}
800801
maxColumnWidth={maxColumnWidth}
801802
></TreeTable>
802-
<form action="/cgi-bin/msaOrthoMCL" target="_blank" method="post">
803+
<ClustalAlignmentForm
804+
action="/cgi-bin/msaOrthoMCL"
805+
sequenceCount={highlightedNodes.length}
806+
sequenceType="proteins"
807+
>
803808
<input type="hidden" name="project_id" value="OrthoMCL" />
804809
{highlightedNodes.map((id) => (
805810
<input type="hidden" name="msa_full_ids" value={id} key={id} />
806811
))}
807-
<p>
808-
Please note: selecting a large number of proteins will take
809-
several minutes to align.
810-
</p>
811812
<div id="userOptions">
812813
<p>
813814
Output format: &nbsp;
@@ -830,7 +831,7 @@ export function RecordTable_Sequences(
830831
)}
831832
</div>
832833
</div>
833-
</form>
834+
</ClustalAlignmentForm>
834835
</Dimmable>
835836
)}
836837
<p>

packages/sites/ortho-site/webpack.config.local.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default configure({
2222
[process.env.MULTI_BLAST_ENDPOINT]: process.env.MULTI_BLAST_URL,
2323
[process.env.DOCUMENTS_ENDPOINT]: process.env.DOCUMENTS_URL,
2424
[process.env.ASSETS_ENDPOINT]: process.env.ASSETS_URL,
25+
[process.env.CGI_BIN_ENDPOINT]: process.env.CGI_BIN_URL,
2526
},
2627
legacyWebAppEndpoint: process.env.LEGACY_WEB_APP_ENDPOINT,
2728
legacyWebAppUrl: process.env.LEGACY_WEB_APP_URL,

0 commit comments

Comments
 (0)