Skip to content

Commit 124c96c

Browse files
authored
[TSPS-721] Add support for boolean input types (#5458)
1 parent c768386 commit 124c96c

File tree

7 files changed

+182
-2
lines changed

7 files changed

+182
-2
lines changed

src/libs/ajax/teaspoons/teaspoons-models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface Pipeline {
77

88
export interface PipelineInput {
99
name: string;
10-
type: 'FILE' | 'STRING' | 'FLOAT';
10+
type: 'FILE' | 'STRING' | 'FLOAT' | 'BOOLEAN';
1111
isRequired: boolean;
1212
displayName?: string;
1313
description?: string;

src/pages/scientificServices/pipelines/tabs/run/RunJob.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe('RunJob Component', () => {
116116
expect(screen.getByText('Enter minimum imputation quality for inclusion')).toBeInTheDocument();
117117
expect(screen.getByText(/Enter description/)).toBeInTheDocument();
118118
expect(screen.getByText('Select a multi-sample VCF file')).toBeInTheDocument();
119+
expect(screen.getByText('Allow chunk failures')).toBeInTheDocument();
119120

120121
// Wait for pipeline options to load
121122
await waitFor(() => {
@@ -162,6 +163,15 @@ describe('RunJob Component', () => {
162163
await user.type(descriptionTextArea, 'Test description for pipeline run');
163164

164165
expect(descriptionTextArea).toHaveValue('Test description for pipeline run');
166+
167+
// Toggle allowChunkFailures
168+
const allowChunkFailuresCheckbox = screen.getByLabelText('Allow chunk failures');
169+
// initially assert unchecked because the default is false
170+
expect(allowChunkFailuresCheckbox).not.toBeChecked();
171+
172+
await user.click(allowChunkFailuresCheckbox);
173+
174+
expect(allowChunkFailuresCheckbox).toBeChecked();
165175
});
166176

167177
it('handles file selection and triggers upload process', async () => {

src/pages/scientificServices/pipelines/tabs/run/RunJob.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from 'src/pages/scientificServices/pipelines/common/scientific-services-common';
1717
import { usePipelinesList } from 'src/pages/scientificServices/pipelines/hooks/usePipelinesList';
1818
import { useUserQuota } from 'src/pages/scientificServices/pipelines/hooks/useUserQuota';
19+
import { PipelineBooleanInput } from 'src/pages/scientificServices/pipelines/tabs/run/inputs/PipelineBooleanInput';
1920
import {
2021
PipelineFileInput,
2122
PipelineInputFileUploadState,
@@ -280,6 +281,25 @@ export const RunJob = () => {
280281
{/* Displays optional run description */}
281282
{selectedPipeline && <PipelineRunDescription value={runDescription} onChange={setRunDescription} />}
282283

284+
{/* Displays all BOOLEAN inputs, one after another */}
285+
{pipelineInputs
286+
.filter((input) => input.type === 'BOOLEAN')
287+
.map((input) => {
288+
return (
289+
<PipelineBooleanInput
290+
value={selectedUserInputs[input.name]}
291+
input={input}
292+
key={`${input.name}`}
293+
onChange={(value) => {
294+
setSelectedUserInputs((prev) => ({
295+
...prev,
296+
[input.name]: value,
297+
}));
298+
}}
299+
/>
300+
);
301+
})}
302+
283303
{/* Displays all FILE inputs, one after another */}
284304
{pipelineInputs
285305
.filter((input) => input.type === 'FILE')
@@ -303,6 +323,7 @@ export const RunJob = () => {
303323
/>
304324
);
305325
})}
326+
306327
{/* Submit button */}
307328
{!submittedJobId && quota && (
308329
<>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
import { PipelineInput } from 'src/libs/ajax/teaspoons/teaspoons-models';
5+
import { renderWithAppContexts as render } from 'src/testing/test-utils';
6+
7+
import { PipelineBooleanInput } from './PipelineBooleanInput';
8+
9+
describe('PipelineBooleanInput', () => {
10+
const mockOnChange = jest.fn();
11+
12+
const basePipelineInput: PipelineInput = {
13+
name: 'testInput',
14+
displayName: 'Test Input',
15+
description: 'This is a test input description',
16+
type: 'BOOLEAN',
17+
defaultValue: 'false',
18+
isRequired: false,
19+
};
20+
21+
it('renders with display name and description', () => {
22+
render(<PipelineBooleanInput input={basePipelineInput} value={false} onChange={mockOnChange} />);
23+
24+
expect(screen.getByText('Test Input')).toBeInTheDocument();
25+
expect(screen.getByText('This is a test input description')).toBeInTheDocument();
26+
});
27+
28+
it('renders with input name when display name is not provided', () => {
29+
const inputWithoutDisplayName = { ...basePipelineInput, displayName: undefined };
30+
render(<PipelineBooleanInput input={inputWithoutDisplayName} value={false} onChange={mockOnChange} />);
31+
32+
expect(screen.getByText('testInput')).toBeInTheDocument();
33+
});
34+
35+
it('renders required indicator when input is required', () => {
36+
const requiredInput = { ...basePipelineInput, isRequired: true };
37+
render(<PipelineBooleanInput input={requiredInput} value={false} onChange={mockOnChange} />);
38+
39+
const requiredIndicator = screen.getByText('*');
40+
expect(requiredIndicator).toBeInTheDocument();
41+
expect(requiredIndicator).toHaveStyle('color: rgb(219, 50, 20)'); // colors.danger()
42+
});
43+
44+
it('does not render required indicator when input is not required', () => {
45+
render(<PipelineBooleanInput input={basePipelineInput} value={false} onChange={mockOnChange} />);
46+
47+
expect(screen.queryByText('*')).not.toBeInTheDocument();
48+
});
49+
50+
it('renders checkbox as unchecked when value is false', () => {
51+
render(<PipelineBooleanInput input={basePipelineInput} value={false} onChange={mockOnChange} />);
52+
53+
const checkbox = screen.getByRole('checkbox');
54+
expect(checkbox).not.toBeChecked();
55+
});
56+
57+
it('renders checkbox as checked when value is true', () => {
58+
render(<PipelineBooleanInput input={basePipelineInput} value onChange={mockOnChange} />);
59+
60+
const checkbox = screen.getByRole('checkbox');
61+
expect(checkbox).toBeChecked();
62+
});
63+
64+
it('uses default value when value prop is undefined', () => {
65+
const inputWithDefaultTrue = { ...basePipelineInput, defaultValue: 'true' };
66+
render(<PipelineBooleanInput input={inputWithDefaultTrue} value={undefined as any} onChange={mockOnChange} />);
67+
68+
const checkbox = screen.getByRole('checkbox');
69+
expect(checkbox).toBeChecked();
70+
});
71+
72+
it('falls back to false when both value and defaultValue are undefined', () => {
73+
const inputWithoutDefault = { ...basePipelineInput, defaultValue: undefined };
74+
render(<PipelineBooleanInput input={inputWithoutDefault} value={undefined as any} onChange={mockOnChange} />);
75+
76+
const checkbox = screen.getByRole('checkbox');
77+
expect(checkbox).not.toBeChecked();
78+
});
79+
80+
it('calls onChange when checkbox is clicked', async () => {
81+
const user = userEvent.setup();
82+
render(<PipelineBooleanInput input={basePipelineInput} value={false} onChange={mockOnChange} />);
83+
84+
const checkbox = screen.getByRole('checkbox');
85+
await user.click(checkbox);
86+
87+
expect(mockOnChange).toHaveBeenCalledWith(true);
88+
});
89+
90+
it('calls onChange with correct value when toggling from true to false', async () => {
91+
const user = userEvent.setup();
92+
render(<PipelineBooleanInput input={basePipelineInput} value onChange={mockOnChange} />);
93+
94+
const checkbox = screen.getByRole('checkbox');
95+
await user.click(checkbox);
96+
97+
expect(mockOnChange).toHaveBeenCalledWith(false);
98+
});
99+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import { LabeledCheckbox } from 'src/components/common';
3+
import { PipelineInput } from 'src/libs/ajax/teaspoons/teaspoons-models';
4+
import colors from 'src/libs/colors';
5+
6+
interface PipelineBooleanInputProps {
7+
input: PipelineInput;
8+
value: boolean;
9+
onChange: (value: boolean) => void;
10+
}
11+
12+
export const PipelineBooleanInput: React.FC<PipelineBooleanInputProps> = ({ input, value, onChange }) => {
13+
const { name, displayName, description, defaultValue, isRequired } = input;
14+
15+
return (
16+
<>
17+
<div style={{ display: 'flex', alignItems: 'center', marginTop: '1.5rem' }}>
18+
<LabeledCheckbox
19+
checked={value ?? defaultValue ?? false}
20+
width={400}
21+
onChange={(e) => {
22+
onChange(e);
23+
}}
24+
>
25+
<div style={{ marginLeft: '0.5rem', fontWeight: 'bold', fontSize: 16 }}>
26+
{displayName || name} {isRequired ? <span style={{ color: colors.danger() }}> *</span> : null}
27+
</div>
28+
</LabeledCheckbox>
29+
</div>
30+
{description && (
31+
<div style={{ marginBottom: '1.5rem', marginTop: '0.5rem', fontStyle: 'italic', maxWidth: 500 }}>
32+
{description}
33+
</div>
34+
)}
35+
</>
36+
);
37+
};

src/pages/scientificServices/pipelines/utils/mock-utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export function mockPipelineWithDetails(name: string): PipelineWithDetails {
3333
minValue: 0,
3434
maxValue: 1,
3535
},
36+
{
37+
name: 'allowChunkFailures',
38+
displayName: 'Allow chunk failures',
39+
description: 'If true, allow up to 10% chunk failure rate. Default false.',
40+
type: 'BOOLEAN',
41+
isRequired: false,
42+
},
3643
{
3744
name: 'multiSampleVcf',
3845
displayName: 'multi-sample VCF file',

src/pages/scientificServices/pipelines/utils/submission-utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export async function preparePipelineRun(
1717
const jobId = crypto.randomUUID();
1818

1919
const finalUserInputs = Object.entries(selectedUserInputs).reduce((acc, [key, value]) => {
20-
acc[key] = value instanceof File ? value.name : value.trim();
20+
if (typeof value === 'boolean') {
21+
acc[key] = value;
22+
} else if (value instanceof File) {
23+
acc[key] = value.name;
24+
} else {
25+
acc[key] = value.trim();
26+
}
2127
return acc;
2228
}, {} as Record<string, any>);
2329

0 commit comments

Comments
 (0)