Skip to content

Commit a9ec84d

Browse files
authored
Make DefaultQuestionForm submit button optionally depend on configurable notebook readiness check (#1602)
* make DefaultQuestionForm submit button optionally depend on configurable notebook readiness check * add isReady for WGCNA notebook and attempted to style required inputs in WdkParamNotebookCell
1 parent cc22a85 commit a9ec84d

File tree

6 files changed

+86
-17
lines changed

6 files changed

+86
-17
lines changed

packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ interface Props {
3838
analysisState: AnalysisState;
3939
notebookType: string;
4040
wdkState: WdkState;
41+
onReadinessChange?: (isReady: boolean) => void;
4142
}
4243

4344
export function EdaNotebookAnalysis(props: Props) {
44-
const { analysisState, notebookType, wdkState } = props;
45+
const { analysisState, notebookType, wdkState, onReadinessChange } = props;
4546
const { analysis, setComputations, addVisualization } = analysisState;
4647

4748
if (analysis == null) throw new Error('Cannot find analysis.');
@@ -50,6 +51,14 @@ export function EdaNotebookAnalysis(props: Props) {
5051
if (notebookPreset == null)
5152
throw new Error(`Cannot find a notebook preset for ${notebookType}`);
5253

54+
useEffect(() => {
55+
if (!onReadinessChange) return;
56+
const ready = notebookPreset.isReady
57+
? notebookPreset.isReady({ analysisState, wdkState })
58+
: true;
59+
onReadinessChange(ready);
60+
}, [analysisState.analysis, wdkState, notebookPreset, onReadinessChange]);
61+
5362
// Check to ensure the notebook is valid for this project
5463
const projectId = useWdkService(
5564
async (wdkService) => (await wdkService.getConfig()).projectId

packages/libs/eda/src/lib/notebook/NotebookPresets.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { NodeData } from '@veupathdb/components/lib/types/plots/network';
77
import { WdkState } from './EdaNotebookAnalysis';
88
import useSnackbar from '@veupathdb/coreui/lib/components/notifications/useSnackbar';
99
import { AnalysisState, CollectionVariableTreeNode } from '../core';
10+
import { plugins } from '../core/components/computations/plugins';
1011
import {
1112
VolcanoPlotConfig,
1213
VolcanoPlotOptions,
@@ -68,14 +69,20 @@ export interface SubsetCellDescriptor
6869
export interface WdkParamCellDescriptor
6970
extends NotebookCellDescriptorBase<'wdkparam'> {
7071
paramNames: string[]; // Param names from the wdk query. These must match exactly or the notebook will err.
72+
requiredParamNames?: string[]; // Subset of paramNames that are required. Labels will be red with an asterisk until filled.
7173
}
7274

75+
export type ReadinessContext =
76+
| { analysisState: AnalysisState; wdkState?: WdkState }
77+
| { analysisState?: AnalysisState; wdkState: WdkState };
78+
7379
type PresetNotebook = {
7480
name: string;
7581
displayName: string;
7682
projects: string[];
7783
cells: NotebookCellDescriptor[];
7884
header?: string; // Optional header text for the notebook, to be displayed above the cells.
85+
isReady?: (context: ReadinessContext) => boolean;
7986
};
8087

8188
// Preset notebooks
@@ -283,6 +290,13 @@ export const presetNotebooks: Record<string, PresetNotebook> = {
283290
],
284291
},
285292
],
293+
isReady: ({ analysisState }) => {
294+
const config = analysisState?.analysis?.descriptor.computations.find(
295+
(c: { descriptor: { type: string } }) =>
296+
c.descriptor.type === 'differentialexpression'
297+
)?.descriptor.configuration;
298+
return plugins['differentialexpression'].isConfigurationComplete(config);
299+
},
286300
},
287301
// WGCNA - only for plasmo. No subsetting cells because of the pre-computed modules and eigengenes.
288302
// Will be primed and prettified in https://github.com/VEuPathDB/web-monorepo/issues/1381
@@ -403,6 +417,7 @@ export const presetNotebooks: Record<string, PresetNotebook> = {
403417
type: 'wdkparam',
404418
title: 'Run gene search',
405419
paramNames: ['wgcnaParam', 'wgcna_correlation_cutoff'],
420+
requiredParamNames: ['wgcnaParam'],
406421
numberedHeader: true,
407422
helperText: (
408423
<span>
@@ -412,6 +427,14 @@ export const presetNotebooks: Record<string, PresetNotebook> = {
412427
),
413428
},
414429
],
430+
isReady: ({ wdkState }) => {
431+
if (!wdkState) return false;
432+
const value = wdkState.paramValues['wgcnaParam'];
433+
// Target wgcnaParam's index-zero makeshift placeholder ("1_choose_module" => "Choose a Module")
434+
if (value == null || value === '' || value.includes('choose_module'))
435+
return false;
436+
return true;
437+
},
415438
},
416439
boxplotNotebook: {
417440
name: 'boxplot',

packages/libs/eda/src/lib/notebook/WdkParamNotebookCell.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function WdkParamNotebookCell(
1414

1515
if (!wdkState) return null;
1616

17-
const { paramNames, title } = cell;
17+
const { paramNames, title, requiredParamNames } = cell;
1818
const { parameters, paramValues, updateParamValue } = wdkState;
1919

2020
// userInputParameters are the wdk parameters that the user will
@@ -40,6 +40,8 @@ export function WdkParamNotebookCell(
4040
<div className="WdkParamInputs">
4141
{userInputParameters.map((param) => {
4242
const paramCurrentValue = paramValues[param.name];
43+
const isRequired =
44+
requiredParamNames?.includes(param.name) ?? false;
4345

4446
// There are many param types. The following is not exhaustive.
4547
if (param.type === 'single-pick-vocabulary') {
@@ -58,7 +60,10 @@ export function WdkParamNotebookCell(
5860

5961
return (
6062
<div className="InputGroup" key={param.name}>
61-
<span>{param.displayName}</span>
63+
<span>
64+
{param.displayName}
65+
{isRequired && <sup>*</sup>}
66+
</span>
6267
<SingleSelect
6368
items={selectItems}
6469
value={paramCurrentValue}
@@ -72,7 +77,10 @@ export function WdkParamNotebookCell(
7277
} else if (param.type === 'string' && param.isNumber) {
7378
return (
7479
<div className="InputGroup" key={param.name}>
75-
<span>{param.displayName}</span>
80+
<span>
81+
{param.displayName}
82+
{isRequired && <sup>*</sup>}
83+
</span>
7684
<NumberInput
7785
value={Number(paramCurrentValue)}
7886
minValue={0} // TO DO: Currently not derived from the parameter, though they should be.

packages/libs/wdk-client/src/Views/Question/DefaultQuestionForm.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export type Props = {
5858
resetFormConfig: ResetFormConfig;
5959
containerClassName?: string;
6060
searchName?: string;
61+
submissionDisabled?: boolean;
6162
};
6263

6364
const cx = makeClassNameHelper('wdk-QuestionForm');
@@ -128,7 +129,8 @@ export default function DefaultQuestionForm(props: Props) {
128129
state.paramsUpdatingDependencies
129130
);
130131

131-
const submissionDisabled = dependentParamsAreUpdating;
132+
const submissionDisabled =
133+
dependentParamsAreUpdating || (props.submissionDisabled ?? false);
132134

133135
const handleSubmit = React.useCallback(
134136
(event: React.FormEvent) => {

packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/questions/EdaNotebookParameter.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ import {
3838

3939
type EdaNotebookParameterProps = {
4040
wdkState: WdkState;
41+
onReadinessChange?: (isReady: boolean) => void;
4142
};
4243

4344
export function EdaNotebookParameter(props: EdaNotebookParameterProps) {
44-
const { wdkState } = props;
45+
const { wdkState, onReadinessChange } = props;
4546
const { parameters, paramValues, updateParamValue } = wdkState;
4647

4748
const studyId = paramValues['eda_dataset_id'];
@@ -102,6 +103,7 @@ export function EdaNotebookParameter(props: EdaNotebookParameterProps) {
102103
analysisState={analysisState}
103104
notebookType={notebookType}
104105
wdkState={wdkState}
106+
onReadinessChange={onReadinessChange}
105107
/>
106108
</CoreUIThemeProvider>
107109
</WorkspaceContainer>
@@ -114,10 +116,11 @@ interface EdaNotebookAdapterProps {
114116
analysisState: AnalysisState;
115117
notebookType: string;
116118
wdkState: WdkState;
119+
onReadinessChange?: (isReady: boolean) => void;
117120
}
118121

119122
function EdaNotebookAdapter(props: EdaNotebookAdapterProps) {
120-
const { analysisState, wdkState, notebookType } = props;
123+
const { analysisState, wdkState, notebookType, onReadinessChange } = props;
121124
const studyId = analysisState.analysis?.studyId;
122125

123126
const analysisClient = useConfiguredAnalysisClient(edaServiceUrl);
@@ -141,6 +144,7 @@ function EdaNotebookAdapter(props: EdaNotebookAdapterProps) {
141144
analysisState={analysisState}
142145
notebookType={notebookType}
143146
wdkState={wdkState}
147+
onReadinessChange={onReadinessChange}
144148
/>
145149
</EDAWorkspaceContainer>
146150
)}
Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import DefaultQuestionForm, {
22
Props,
33
} from '@veupathdb/wdk-client/lib/Views/Question/DefaultQuestionForm';
4-
import React, { useCallback } from 'react';
4+
import React, { useCallback, useMemo, useState } from 'react';
55
import { EdaNotebookParameter } from './EdaNotebookParameter';
66
import { Parameter } from '@veupathdb/wdk-client/lib/Utils/WdkModel';
77
import { WdkState } from '@veupathdb/eda/lib/notebook/EdaNotebookAnalysis';
8+
import { presetNotebooks } from '@veupathdb/eda/lib/notebook/NotebookPresets';
89

910
export const EdaNotebookQuestionForm = (props: Props) => {
1011
const { searchName } = props;
1112
if (!searchName) {
1213
throw new Error('No search defined.');
1314
}
1415

16+
const notebookType =
17+
props.state.question.properties?.['edaNotebookType']?.[0];
18+
const preset = notebookType ? presetNotebooks[notebookType] : undefined;
19+
20+
// Start disabled only if the preset has a readiness check
21+
const [notebookReady, setNotebookReady] = useState(!preset?.isReady);
22+
1523
// We'll use this function throughout the notebook to update any wdk parameters.
1624
const updateParamValue = useCallback(
1725
(parameter: Parameter, newParamValue: string) => {
@@ -25,27 +33,42 @@ export const EdaNotebookQuestionForm = (props: Props) => {
2533
[props.eventHandlers, searchName]
2634
);
2735

28-
const wdkState: WdkState = {
29-
// Safe: pluginConfig.tsx only routes here when edaNotebookType property is present
30-
queryName: props.state.question.queryName!,
31-
parameters: props.state.question.parameters,
32-
paramValues: props.state.paramValues,
33-
updateParamValue,
34-
questionProperties: props.state.question.properties ?? {},
35-
};
36+
const wdkState = useMemo<WdkState>(
37+
() => ({
38+
// Safe: pluginConfig.tsx only routes here when edaNotebookType property is present
39+
queryName: props.state.question.queryName!,
40+
parameters: props.state.question.parameters,
41+
paramValues: props.state.paramValues,
42+
updateParamValue,
43+
questionProperties: props.state.question.properties ?? {},
44+
}),
45+
[
46+
props.state.question.queryName,
47+
props.state.question.parameters,
48+
props.state.paramValues,
49+
updateParamValue,
50+
props.state.question.properties,
51+
]
52+
);
3653

3754
// An override that renders the notebook instead of any default parameter or parameter group ui.
3855
// NOTE: this function is run for every visible parameter group. May cause
3956
// an issue if the wdk question has multiple parameter groups.
4057
const renderParamGroup = () => {
41-
return <EdaNotebookParameter wdkState={wdkState} />;
58+
return (
59+
<EdaNotebookParameter
60+
wdkState={wdkState}
61+
onReadinessChange={setNotebookReady}
62+
/>
63+
);
4264
};
4365

4466
return (
4567
<DefaultQuestionForm
4668
{...props}
4769
renderParamGroup={renderParamGroup}
4870
resetFormConfig={{ offered: false }}
71+
submissionDisabled={!notebookReady}
4972
/>
5073
);
5174
};

0 commit comments

Comments
 (0)