11import { type FC , useId , useMemo } from 'react' ;
22
3+ import type { LapisSearchParameters } from './DownloadDialog/SequenceFilters.tsx' ;
4+ import { createOptionsProviderHook , type OptionsProvider } from './fields/AutoCompleteOptions.ts' ;
35import { type ReferenceGenomesInfo } from '../../types/referencesGenomes.ts' ;
6+ import { formatNumberWithDefaultLocale } from '../../utils/formatNumber.tsx' ;
47import { getReferenceIdentifier } from '../../utils/referenceSelection.ts' ;
58import type { MetadataFilterSchema } from '../../utils/search.ts' ;
6- import {
7- getSegmentNames ,
8- segmentsWithMultipleReferences ,
9- type SegmentReferenceSelections ,
10- } from '../../utils/sequenceTypeHelpers.ts' ;
9+ import { segmentsWithMultipleReferences , type SegmentReferenceSelections } from '../../utils/sequenceTypeHelpers.ts' ;
1110import { Button } from '../common/Button.tsx' ;
1211import { Select } from '../common/Select.tsx' ;
1312import MaterialSymbolsClose from '~icons/material-symbols/close' ;
1413
14+ type SegmentReferenceSelectorProps = {
15+ selectId : string ;
16+ label : string | undefined ;
17+ value : string | null | undefined ;
18+ onChange : ( next : string ) => void ;
19+ onClear : ( ) => void ;
20+ optionsProvider : OptionsProvider ;
21+ } ;
22+
23+ const SegmentReferenceSelector : FC < SegmentReferenceSelectorProps > = ( {
24+ selectId,
25+ label,
26+ value,
27+ onChange,
28+ onClear,
29+ optionsProvider,
30+ } ) => {
31+ const hook = createOptionsProviderHook ( optionsProvider ) ;
32+ const { options, isPending, error, load } = hook ( ) ;
33+
34+ return (
35+ < div className = 'bg-gray-50 border border-gray-300 rounded-md p-3 mb-3' >
36+ < label className = 'block text-xs font-semibold text-gray-700 mb-1' htmlFor = { selectId } >
37+ { label }
38+ </ label >
39+
40+ < div className = 'relative' >
41+ < Select
42+ id = { selectId }
43+ value = { value ?? '' }
44+ onChange = { ( e ) => onChange ( e . target . value ) }
45+ onFocus = { ( ) => {
46+ load ( ) ;
47+ } }
48+ className = 'w-full px-2 py-1.5 rounded border border-gray-300 text-sm bg-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-200'
49+ >
50+ < option value = '' disabled >
51+ Select { formatLabel ( label ?? '' ) } ...
52+ </ option >
53+
54+ { isPending && (
55+ < option value = '' disabled >
56+ Loading...
57+ </ option >
58+ ) }
59+
60+ { error && ! isPending && (
61+ < option value = '' disabled >
62+ Failed to load options
63+ </ option >
64+ ) }
65+
66+ { ! isPending &&
67+ ! error &&
68+ options . map ( ( opt ) => {
69+ const display =
70+ opt . count !== undefined
71+ ? `${ opt . option } (${ formatNumberWithDefaultLocale ( opt . count ) } )`
72+ : opt . option ;
73+
74+ return (
75+ < option key = { opt . value } value = { opt . value } >
76+ { display }
77+ </ option >
78+ ) ;
79+ } ) }
80+ </ Select >
81+
82+ { value != null && (
83+ < Button
84+ className = 'absolute top-2 right-6 flex items-center pr-2 h-5 bg-white rounded-sm'
85+ onClick = { onClear }
86+ aria-label = { `Clear ${ label ?? '' } ` }
87+ type = 'button'
88+ >
89+ < MaterialSymbolsClose className = 'w-5 h-5 text-gray-400' />
90+ </ Button >
91+ ) }
92+ </ div >
93+
94+ < p className = 'text-xs text-gray-600 mt-2' >
95+ Select a { formatLabel ( label ?? '' ) } to enable mutation search and download of aligned sequences
96+ </ p >
97+ </ div >
98+ ) ;
99+ } ;
100+
15101type ReferenceSelectorProps = {
102+ lapisSearchParameters : LapisSearchParameters ;
103+ lapisUrl : string ;
16104 filterSchema : MetadataFilterSchema ;
17105 referenceGenomesInfo : ReferenceGenomesInfo ;
18106 referenceIdentifierField : string ;
19- setSelectedReferences : ( newValues : SegmentReferenceSelections ) => void ;
20107 selectedReferences : SegmentReferenceSelections ;
108+ setSelectedReferences : ( newValues : SegmentReferenceSelections ) => void ;
21109} ;
22110
23- /**
24- * In the multi pathogen case, this is a prominent selector at the top to choose the reference.
25- * Choosing a value here is required e.g. to enable mutation search and download of aligned sequences.
26- *
27- * Does nothing in the single pathogen case.
28- */
29111export const ReferenceSelector : FC < ReferenceSelectorProps > = ( {
112+ lapisSearchParameters,
113+ lapisUrl,
30114 filterSchema,
31115 referenceGenomesInfo,
32116 referenceIdentifierField,
@@ -35,20 +119,25 @@ export const ReferenceSelector: FC<ReferenceSelectorProps> = ({
35119} ) => {
36120 const baseSelectId = useId ( ) ;
37121
38- const segments = getSegmentNames ( referenceGenomesInfo ) ;
39-
40- if ( segmentsWithMultipleReferences ( referenceGenomesInfo ) . length === 0 ) {
41- return null ;
42- }
43-
44- const labelsBySegment = useMemo ( ( ) => {
45- return segments . reduce < Record < string , string | undefined > > ( ( acc , segmentName ) => {
122+ const multiRefSegments = segmentsWithMultipleReferences ( referenceGenomesInfo ) ;
123+ if ( multiRefSegments . length === 0 ) return null ;
124+ const identifierBySegment = useMemo ( ( ) => {
125+ return multiRefSegments . reduce < Record < string , string | undefined > > ( ( acc , segmentName ) => {
46126 const identifier = getReferenceIdentifier (
47127 referenceIdentifierField ,
48128 segmentName ,
49129 referenceGenomesInfo . isMultiSegmented ,
50130 ) ;
51131
132+ acc [ segmentName ] = identifier ;
133+
134+ return acc ;
135+ } , { } ) ;
136+ } , [ filterSchema , referenceIdentifierField , referenceGenomesInfo ] ) ;
137+ const labelsBySegment = useMemo ( ( ) => {
138+ return multiRefSegments . reduce < Record < string , string | undefined > > ( ( acc , segmentName ) => {
139+ const identifier = identifierBySegment [ segmentName ] ;
140+
52141 acc [ segmentName ] = identifier ? filterSchema . filterNameToLabelMap ( ) [ identifier ] : undefined ;
53142
54143 return acc ;
@@ -57,60 +146,41 @@ export const ReferenceSelector: FC<ReferenceSelectorProps> = ({
57146
58147 return (
59148 < >
60- { segmentsWithMultipleReferences ( referenceGenomesInfo ) . map ( ( segment ) => {
149+ { multiRefSegments . map ( ( segment ) => {
61150 const selectId = `${ baseSelectId } -${ segment } ` ;
151+ const identifier = identifierBySegment [ segment ] ;
152+ const label = labelsBySegment [ segment ] ;
62153
63- return (
64- < div key = { segment } className = 'bg-gray-50 border border-gray-300 rounded-md p-3 mb-3' >
65- < label className = 'block text-xs font-semibold text-gray-700 mb-1' htmlFor = { selectId } >
66- { labelsBySegment [ segment ] }
67- </ label >
68-
69- < div className = 'relative' >
70- < Select
71- id = { selectId }
72- value = { selectedReferences [ segment ] ?? '' }
73- onChange = { ( e ) =>
74- setSelectedReferences ( {
75- ...selectedReferences ,
76- [ segment ] : e . target . value ,
77- } )
78- }
79- className = 'w-full px-2 py-1.5 rounded border border-gray-300 text-sm bg-white focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-200'
80- >
81- < option value = '' disabled >
82- Select { formatLabel ( labelsBySegment [ segment ] ?? '' ) } ...
83- </ option >
154+ const optionsProvider = useMemo (
155+ ( ) => ( {
156+ type : 'generic' as const ,
157+ lapisUrl,
158+ lapisSearchParameters,
159+ fieldName : identifier ! ,
160+ } ) ,
161+ [ lapisUrl , lapisSearchParameters , identifier ] ,
162+ ) ;
84163
85- { Object . keys ( referenceGenomesInfo . segmentReferenceGenomes [ segment ] ) . map ( ( reference ) => (
86- < option key = { reference } value = { reference } >
87- { reference }
88- </ option >
89- ) ) }
90- </ Select >
91-
92- { selectedReferences [ segment ] != null && (
93- < Button
94- className = 'absolute top-2 right-6 flex items-center pr-2 h-5 bg-white rounded-sm'
95- onClick = { ( ) =>
96- setSelectedReferences ( {
97- ...selectedReferences ,
98- [ segment ] : null ,
99- } )
100- }
101- aria-label = { `Clear ${ labelsBySegment [ segment ] ?? '' } ` }
102- type = 'button'
103- >
104- < MaterialSymbolsClose className = 'w-5 h-5 text-gray-400' />
105- </ Button >
106- ) }
107- </ div >
108-
109- < p className = 'text-xs text-gray-600 mt-2' >
110- Select a { formatLabel ( labelsBySegment [ segment ] ?? '' ) } to enable mutation search and
111- download of aligned sequences
112- </ p >
113- </ div >
164+ return (
165+ < SegmentReferenceSelector
166+ key = { segment }
167+ label = { label }
168+ selectId = { selectId }
169+ value = { selectedReferences [ segment ] }
170+ onChange = { ( e ) =>
171+ setSelectedReferences ( {
172+ ...selectedReferences ,
173+ [ segment ] : e ,
174+ } )
175+ }
176+ onClear = { ( ) =>
177+ setSelectedReferences ( {
178+ ...selectedReferences ,
179+ [ segment ] : null ,
180+ } )
181+ }
182+ optionsProvider = { optionsProvider }
183+ />
114184 ) ;
115185 } ) }
116186 </ >
0 commit comments