Skip to content

Commit a51de56

Browse files
authored
Max recommended organisms gate (#1571)
* Added useMaxRecommendedGate hook and applied to OrganismParam variants * tidy up unused selectedValues * model-configurable maxRecommendedMsg and other improvements
1 parent 398ff2e commit a51de56

3 files changed

Lines changed: 232 additions & 50 deletions

File tree

packages/libs/preferred-organisms/src/lib/components/OrganismParam.tsx

Lines changed: 91 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
usePreferredSpecies,
5151
} from '../hooks/preferredOrganisms';
5252
import { useReferenceStrains } from '../hooks/referenceStrains';
53+
import { useMaxRecommendedGate } from '../hooks/maxRecommendedGate';
5354

5455
import { OrganismPreferencesWarning } from './OrganismPreferencesWarning';
5556

@@ -64,16 +65,20 @@ export const SHOW_ONLY_PREFERRED_ORGANISMS_PROPERTY =
6465
export const HIGHLIGHT_REFERENCE_ORGANISMS_PROPERTY =
6566
'highlightReferenceOrganisms';
6667
export const IS_SPECIES_PARAM_PROPERTY = 'isSpeciesParam';
68+
export const MAX_RECOMMENDED_PROPERTY = 'maxRecommended';
69+
export const MAX_RECOMMENDED_MSG_PROPERTY = 'maxRecommendedMsg';
6770

68-
interface OrganismParamProps<T extends Parameter, S = void>
69-
extends DefaultParamProps<T, S> {
71+
interface OrganismParamProps<
72+
T extends Parameter,
73+
S = void,
74+
> extends DefaultParamProps<T, S> {
7075
isSearchPage?: boolean;
7176
}
7277

7378
export function OrganismParam(props: OrganismParamProps<Parameter, State>) {
7479
if (!isOrganismParamProps(props)) {
7580
throw new Error(
76-
`Tried to render non-organism parameter ${props.parameter.name} with OrganismParam.`
81+
`Tried to render non-organism parameter ${props.parameter.name} with OrganismParam.`,
7782
);
7883
}
7984

@@ -87,7 +92,7 @@ export function OrganismParam(props: OrganismParamProps<Parameter, State>) {
8792
}
8893

8994
export function ValidatedOrganismParam(
90-
props: OrganismParamProps<EnumParam, State>
95+
props: OrganismParamProps<EnumParam, State>,
9196
) {
9297
return props.parameter.displayType === 'treeBox' ? (
9398
<TreeBoxOrganismEnumParam
@@ -101,18 +106,31 @@ export function ValidatedOrganismParam(
101106
}
102107

103108
function TreeBoxOrganismEnumParam(
104-
props: OrganismParamProps<TreeBoxEnumParam, State>
109+
props: OrganismParamProps<TreeBoxEnumParam, State>,
105110
) {
106111
const [showOnlyReferenceOrganisms, setShowOnlyReferenceOrganisms] =
107112
useState<boolean>(false);
108113

109114
const { selectedValues, onChange } = useEnumParamSelectedValues(props);
110115

116+
// Extract maxRecommended and apply the gate
117+
const maxRecommended = Number(
118+
props.parameter.properties?.[MAX_RECOMMENDED_PROPERTY]?.[0],
119+
);
120+
const maxRecommendedMsg =
121+
props.parameter.properties?.[MAX_RECOMMENDED_MSG_PROPERTY]?.[0];
122+
123+
const { wrappedOnChange, modalElement } = useMaxRecommendedGate(
124+
onChange,
125+
maxRecommended,
126+
maxRecommendedMsg,
127+
);
128+
111129
const paramWithPrunedVocab = useTreeBoxParamWithPrunedVocab(
112130
props.parameter,
113131
selectedValues,
114-
onChange,
115-
props.isSearchPage
132+
wrappedOnChange,
133+
props.isSearchPage,
116134
);
117135

118136
const { maxSelectedCount } = paramWithPrunedVocab;
@@ -121,12 +139,12 @@ function TreeBoxOrganismEnumParam(
121139

122140
const shouldHighlightReferenceOrganisms =
123141
props.parameter.properties?.[ORGANISM_PROPERTIES_KEY].includes(
124-
HIGHLIGHT_REFERENCE_ORGANISMS_PROPERTY
142+
HIGHLIGHT_REFERENCE_ORGANISMS_PROPERTY,
125143
) ?? false;
126144

127145
const renderNode = useRenderOrganismNode(
128146
shouldHighlightReferenceOrganisms ? referenceStrains : undefined,
129-
undefined
147+
undefined,
130148
);
131149
const searchPredicate = useOrganismSearchPredicate(referenceStrains);
132150

@@ -176,52 +194,75 @@ function TreeBoxOrganismEnumParam(
176194
searchPredicate,
177195
showOnlyReferenceOrganisms,
178196
referenceStrains,
179-
]
197+
],
180198
);
181199

182-
return hasEmptyVocabularly(paramWithPrunedVocab) ? (
183-
<EmptyParamWarning />
184-
) : (
185-
<TreeBoxEnumParamComponent
186-
{...props}
187-
selectedValues={selectedValues}
188-
onChange={onChange}
189-
context={props.ctx}
190-
parameter={paramWithPrunedVocab}
191-
wrapCheckboxTreeProps={wrapCheckboxTreeProps}
192-
/>
200+
return (
201+
<>
202+
{hasEmptyVocabularly(paramWithPrunedVocab) ? (
203+
<EmptyParamWarning />
204+
) : (
205+
<TreeBoxEnumParamComponent
206+
{...props}
207+
selectedValues={selectedValues}
208+
onChange={wrappedOnChange}
209+
context={props.ctx}
210+
parameter={paramWithPrunedVocab}
211+
wrapCheckboxTreeProps={wrapCheckboxTreeProps}
212+
/>
213+
)}
214+
{modalElement}
215+
</>
193216
);
194217
}
195218

196219
function FlatOrganismEnumParam(
197-
props: OrganismParamProps<FlatEnumParam, State>
220+
props: OrganismParamProps<FlatEnumParam, State>,
198221
) {
199222
const { selectedValues, onChange } = useEnumParamSelectedValues(props);
200223

224+
// Extract maxRecommended and apply the gate
225+
const maxRecommended = Number(
226+
props.parameter.properties?.[MAX_RECOMMENDED_PROPERTY]?.[0],
227+
);
228+
const maxRecommendedMsg =
229+
props.parameter.properties?.[MAX_RECOMMENDED_MSG_PROPERTY]?.[0];
230+
231+
const { wrappedOnChange, modalElement } = useMaxRecommendedGate(
232+
onChange,
233+
maxRecommended,
234+
maxRecommendedMsg,
235+
);
236+
201237
const paramWithPrunedVocab = useFlatParamWithPrunedVocab(
202238
props.parameter,
203239
selectedValues,
204-
onChange,
205-
props.isSearchPage
240+
wrappedOnChange,
241+
props.isSearchPage,
206242
);
207243

208-
return hasEmptyVocabularly(paramWithPrunedVocab) ? (
209-
<EmptyParamWarning />
210-
) : (
211-
<ParamComponent {...props} parameter={paramWithPrunedVocab} />
244+
return (
245+
<>
246+
{hasEmptyVocabularly(paramWithPrunedVocab) ? (
247+
<EmptyParamWarning />
248+
) : (
249+
<ParamComponent {...props} parameter={paramWithPrunedVocab} />
250+
)}
251+
{modalElement}
252+
</>
212253
);
213254
}
214255

215256
function useTreeBoxParamWithPrunedVocab(
216257
parameter: TreeBoxEnumParam,
217258
selectedValues: string[],
218259
onChange: (newValue: string[]) => void,
219-
isSearchPage?: boolean
260+
isSearchPage?: boolean,
220261
) {
221262
const preferredValues = usePreferredValues(
222263
parameter,
223264
selectedValues,
224-
isSearchPage
265+
isSearchPage,
225266
);
226267

227268
const [preferredOrganismsEnabled] = usePreferredOrganismsEnabledState();
@@ -243,7 +284,7 @@ function useTreeBoxParamWithPrunedVocab(
243284
? pruneDescendantNodes(
244285
(node) =>
245286
node.children.length > 0 || preferredValues.has(node.data.term),
246-
prunedVocabulary
287+
prunedVocabulary,
247288
)
248289
: prunedVocabulary;
249290

@@ -259,7 +300,7 @@ function useTreeBoxParamWithPrunedVocab(
259300
selectedValues,
260301
onChange,
261302
preferredValues,
262-
paramWithPrunedVocab
303+
paramWithPrunedVocab,
263304
);
264305

265306
return paramWithPrunedVocab;
@@ -269,12 +310,12 @@ function useFlatParamWithPrunedVocab(
269310
parameter: FlatEnumParam,
270311
selectedValues: string[],
271312
onChange: (newValue: string[]) => void,
272-
isSearchPage?: boolean
313+
isSearchPage?: boolean,
273314
) {
274315
const preferredValues = usePreferredValues(
275316
parameter,
276317
selectedValues,
277-
isSearchPage
318+
isSearchPage,
278319
);
279320

280321
const [preferredOrganismsEnabled] = usePreferredOrganismsEnabledState();
@@ -287,7 +328,7 @@ function useFlatParamWithPrunedVocab(
287328
? {
288329
...parameter,
289330
vocabulary: parameter.vocabulary.filter(([term]) =>
290-
preferredValues.has(term)
331+
preferredValues.has(term),
291332
),
292333
}
293334
: parameter;
@@ -297,23 +338,23 @@ function useFlatParamWithPrunedVocab(
297338
selectedValues,
298339
onChange,
299340
preferredValues,
300-
paramWithPrunedVocab
341+
paramWithPrunedVocab,
301342
);
302343

303344
return paramWithPrunedVocab;
304345
}
305346

306347
function useEnumParamSelectedValues(
307-
props: OrganismParamProps<EnumParam, State>
348+
props: OrganismParamProps<EnumParam, State>,
308349
) {
309350
const paramIsMultiPick = isMultiPick(props.parameter);
310351

311352
const selectedValues = useMemo(() => {
312353
return paramIsMultiPick
313354
? toMultiValueArray(props.value)
314355
: props.value == null || props.value === ''
315-
? []
316-
: [props.value];
356+
? []
357+
: [props.value];
317358
}, [paramIsMultiPick, props.value]);
318359

319360
const transformValue = useCallback(
@@ -324,7 +365,7 @@ function useEnumParamSelectedValues(
324365
return newValue.length === 0 ? '' : newValue[0];
325366
}
326367
},
327-
[paramIsMultiPick]
368+
[paramIsMultiPick],
328369
);
329370

330371
const onParamValueChange = props.onParamValueChange;
@@ -333,7 +374,7 @@ function useEnumParamSelectedValues(
333374
(newValue: string[]) => {
334375
onParamValueChange(transformValue(newValue));
335376
},
336-
[onParamValueChange, transformValue]
377+
[onParamValueChange, transformValue],
337378
);
338379

339380
return {
@@ -345,7 +386,7 @@ function useEnumParamSelectedValues(
345386
function usePreferredValues(
346387
parameter: EnumParam,
347388
selectedValues: string[],
348-
isSearchPageProp?: boolean
389+
isSearchPageProp?: boolean,
349390
) {
350391
const [preferredOrganisms] = usePreferredOrganismsState();
351392
const preferredSpecies = usePreferredSpecies();
@@ -372,9 +413,9 @@ function usePreferredValues(
372413
initialSelectedValuesRef.current,
373414
parameter.vocabulary,
374415
isSearchPage,
375-
findPreferenceType(parameter)
416+
findPreferenceType(parameter),
376417
),
377-
[parameter, isSearchPage, preferredOrganisms, preferredSpecies]
418+
[parameter, isSearchPage, preferredOrganisms, preferredSpecies],
378419
);
379420

380421
return preferredValues;
@@ -384,7 +425,7 @@ function useRestrictSelectedValues(
384425
selectedValues: string[],
385426
onChange: (newValue: string[]) => void,
386427
preferredValues: Set<string>,
387-
parameter: EnumParam
428+
parameter: EnumParam,
388429
) {
389430
const [preferredOrganismsEnabled] = usePreferredOrganismsEnabledState();
390431

@@ -409,7 +450,7 @@ function useRestrictSelectedValues(
409450
!hasEmptyVocabularly(parameter)
410451
) {
411452
const preferredSelectedValues = selectedValues.filter((selectedValue) =>
412-
preferredValues.has(selectedValue)
453+
preferredValues.has(selectedValue),
413454
);
414455

415456
if (preferredSelectedValues.length !== selectedValues.length) {
@@ -427,7 +468,7 @@ function useRestrictSelectedValues(
427468
}
428469

429470
function isOrganismParamProps<S = void>(
430-
props: OrganismParamProps<Parameter, S>
471+
props: OrganismParamProps<Parameter, S>,
431472
): props is OrganismParamProps<EnumParam, S> {
432473
return isPropsType(props, isOrganismParam);
433474
}
@@ -441,7 +482,7 @@ export function isOrganismParam(parameter: Parameter): parameter is EnumParam {
441482

442483
function findShouldOnlyShowPreferredOrganisms(parameter: Parameter) {
443484
return parameter.properties?.[ORGANISM_PROPERTIES_KEY].includes(
444-
SHOW_ONLY_PREFERRED_ORGANISMS_PROPERTY
485+
SHOW_ONLY_PREFERRED_ORGANISMS_PROPERTY,
445486
);
446487
}
447488

@@ -465,7 +506,7 @@ function findPreferredValues(
465506
selectedValues: string[],
466507
vocabulary: EnumParam['vocabulary'],
467508
isSearchPage: boolean,
468-
preferenceType: 'organism' | 'species'
509+
preferenceType: 'organism' | 'species',
469510
) {
470511
const basePreferredValues =
471512
preferenceType === 'organism' ? preferredOrganismValues : preferredSpecies;
@@ -481,7 +522,7 @@ function findPreferredValues(
481522

482523
function findPreferredDescendants(
483524
vocabRoot: TreeBoxVocabNode,
484-
preferredValues: Set<string>
525+
preferredValues: Set<string>,
485526
) {
486527
const preferredDescendants = new Set<string>();
487528

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.MaxRecommendedGate {
2+
padding: 1em;
3+
width: 450px;
4+
display: grid;
5+
6+
&--Message {
7+
> div p {
8+
font-size: 1.2em;
9+
justify-self: center;
10+
text-align: center;
11+
}
12+
}
13+
14+
&--Buttons {
15+
margin-top: 3em;
16+
display: flex;
17+
justify-content: space-between;
18+
}
19+
}

0 commit comments

Comments
 (0)