99 placeholder =" Search accession or title..."
1010 />
1111 </div >
12- <span class =" result-count" >{{ filteredDatasets .length }} dataset{{ filteredDatasets .length !== 1 ? 's' : '' }}</span >
12+ <span class =" result-count" >{{ rows .length }} dataset{{ rows .length !== 1 ? 's' : '' }}</span >
1313 </div >
1414
1515 <div v-if =" loading" style =" text-align :center ; padding : 48px 0 ; color : var (--text-muted );" >
2020 <table class =" dataset-table" >
2121 <thead >
2222 <tr >
23- <th @click =" sortBy ('accession')" style = " cursor : pointer ; user-select : none ; " >
24- Accession < span class = " sort-icon " > {{ sortIcon ('accession') }}</ span >
23+ <th class = " sortable " @click =" clickSort ('accession')" >
24+ Accession {{ icon ('accession') }}
2525 </th >
26- <th @click =" sortBy ('title')" style = " cursor : pointer ; user-select : none ; " >
27- Title < span class = " sort-icon " > {{ sortIcon ('title') }}</ span >
26+ <th class = " sortable " @click =" clickSort ('title')" >
27+ Title {{ icon ('title') }}
2828 </th >
2929 <template v-if =" isMsnet " >
30- <th @click =" sortBy ('species')" style = " cursor : pointer ; user-select : none ; text-align : left ; " >
31- Species < span class = " sort-icon " > {{ sortIcon ('species') }}</ span >
30+ <th class = " sortable " @click =" clickSort ('species')" >
31+ Species {{ icon ('species') }}
3232 </th >
33- <th @click =" sortBy ('instrument')" style = " cursor : pointer ; user-select : none ; text-align : left ; " >
34- Instrument < span class = " sort-icon " > {{ sortIcon ('instrument') }}</ span >
33+ <th class = " sortable " @click =" clickSort ('instrument')" >
34+ Instrument {{ icon ('instrument') }}
3535 </th >
36- <th @click =" sortBy ('psm_count')" style = " cursor : pointer ; user-select : none ; text-align : right ; " >
37- PSMs < span class = " sort-icon " > {{ sortIcon ('psm_count') }}</ span >
36+ <th class = " sortable num " @click =" clickSort ('psm_count')" >
37+ PSMs {{ icon ('psm_count') }}
3838 </th >
39- <th @click =" sortBy ('runs')" style = " cursor : pointer ; user-select : none ; text-align : right ; " >
40- Runs < span class = " sort-icon " > {{ sortIcon ('runs') }}</ span >
39+ <th class = " sortable num " @click =" clickSort ('runs')" >
40+ Runs {{ icon ('runs') }}
4141 </th >
4242 </template >
4343 <template v-else >
44- <th @click =" sortBy ('samples')" style = " cursor : pointer ; user-select : none ; text-align : right ; " >
45- Samples < span class = " sort-icon " > {{ sortIcon ('samples') }}</ span >
44+ <th class = " sortable num " @click =" clickSort ('samples')" >
45+ Samples {{ icon ('samples') }}
4646 </th >
47- <th @click =" sortBy ('runs')" style = " cursor : pointer ; user-select : none ; text-align : right ; " >
48- Runs < span class = " sort-icon " > {{ sortIcon ('runs') }}</ span >
47+ <th class = " sortable num " @click =" clickSort ('runs')" >
48+ Runs {{ icon ('runs') }}
4949 </th >
50- <th @click =" sortBy ('proteins')" style = " cursor : pointer ; user-select : none ; text-align : right ; " >
51- Proteins < span class = " sort-icon " > {{ sortIcon ('proteins') }}</ span >
50+ <th class = " sortable num " @click =" clickSort ('proteins')" >
51+ Proteins {{ icon ('proteins') }}
5252 </th >
53- <th @click =" sortBy ('peptides')" style = " cursor : pointer ; user-select : none ; text-align : right ; " >
54- Peptides < span class = " sort-icon " > {{ sortIcon ('peptides') }}</ span >
53+ <th class = " sortable num " @click =" clickSort ('peptides')" >
54+ Peptides {{ icon ('peptides') }}
5555 </th >
5656 </template >
5757 </tr >
58- <!-- Filter row for MSNet collections -->
5958 <tr v-if =" isMsnet" class =" filter-row" >
6059 <th ></th >
6160 <th ></th >
6261 <th >
6362 <select v-model =" filterSpecies" class =" col-filter" >
64- <option value =" " >All</option >
63+ <option value =" " >All species </option >
6564 <option v-for =" s in uniqueSpecies" :key =" s" :value =" s" >{{ s }}</option >
6665 </select >
6766 </th >
6867 <th >
6968 <select v-model =" filterInstrument" class =" col-filter" >
70- <option value =" " >All</option >
69+ <option value =" " >All instruments </option >
7170 <option v-for =" i in uniqueInstruments" :key =" i" :value =" i" >{{ i }}</option >
7271 </select >
7372 </th >
7675 </tr >
7776 </thead >
7877 <tbody >
79- <tr v-if =" filteredDatasets .length === 0" >
78+ <tr v-if =" rows .length === 0" >
8079 <td :colspan =" isMsnet ? 6 : 6" style =" text-align :center ; padding : 32px ; color : var (--text-muted );" >
8180 No datasets found.
8281 </td >
8382 </tr >
8483 <tr
85- v-for =" ds in sortedDatasets "
86- :key =" ds.accession + (ds._idx || '') "
84+ v-for =" (ds, idx) in rows "
85+ :key =" ds.accession + '-' + idx "
8786 style =" cursor :pointer ;"
8887 @click =" navigateTo(ds)"
8988 >
113112 <span v-else style =" color :var (--text-muted );" >—</span >
114113 </td >
115114 <td style =" font-size :13px ; color : var (--text-secondary );" >{{ ds.instrument || '—' }}</td >
116- <td class =" td-num" >{{ formatNum (ds.psm_count) }}</td >
117- <td class =" td-num" >{{ formatNum (ds.runs) }}</td >
115+ <td class =" td-num" >{{ fmtNum (ds.psm_count) }}</td >
116+ <td class =" td-num" >{{ fmtNum (ds.runs) }}</td >
118117 </template >
119118 <template v-else >
120- <td class =" td-num" >{{ formatNum (ds.samples) }}</td >
121- <td class =" td-num" >{{ formatNum (ds.runs) }}</td >
122- <td class =" td-num" >{{ formatNum (ds.proteins) }}</td >
123- <td class =" td-num" >{{ formatNum (ds.peptides) }}</td >
119+ <td class =" td-num" >{{ fmtNum (ds.samples) }}</td >
120+ <td class =" td-num" >{{ fmtNum (ds.runs) }}</td >
121+ <td class =" td-num" >{{ fmtNum (ds.proteins) }}</td >
122+ <td class =" td-num" >{{ fmtNum (ds.peptides) }}</td >
124123 </template >
125124 </tr >
126125 </tbody >
@@ -142,128 +141,110 @@ const props = defineProps({
142141
143142const router = useRouter ()
144143const searchQuery = ref (' ' )
145- const sortKey = ref (' ' )
146- const sortDir = ref (- 1 ) // -1 = descending (biggest first, natural for counts)
147-
148- // Column filters (MSNet only)
149144const filterSpecies = ref (' ' )
150145const filterInstrument = ref (' ' )
151146
152- const isMsnet = computed (() => props .collectionName === ' msnet' )
147+ // Sort state: key='' means unsorted
148+ const sKey = ref (' ' )
149+ const sAsc = ref (true )
153150
154- // Numeric columns that must be sorted numerically
155- const NUMERIC_COLS = new Set ([' psm_count' , ' runs' , ' samples' , ' proteins' , ' peptides' ])
151+ const isMsnet = computed (() => props . collectionName === ' msnet ' )
152+ const NUM = new Set ([' psm_count' , ' runs' , ' samples' , ' proteins' , ' peptides' ])
156153
157- // Reset column filters when collection changes
158154watch (() => props .collectionName , () => {
159155 filterSpecies .value = ' '
160156 filterInstrument .value = ' '
161157 searchQuery .value = ' '
158+ sKey .value = ' '
162159})
163160
164- // Unique sorted values for dropdowns — derived from raw dataset (before any filter)
165161const uniqueSpecies = computed (() => {
166- const vals = new Set ()
167- for ( const ds of props .datasets ) {
168- const s = ds .species || (ds .organisms && ds .organisms [0 ]) || ' '
169- if (s) vals .add (s )
170- }
171- return [... vals ].sort (( a , b ) => a . localeCompare (b) )
162+ const s = new Set ()
163+ props .datasets . forEach ( d => {
164+ const v = d .species || (d .organisms && d .organisms [0 ]) || ' '
165+ if (v) s .add (v )
166+ })
167+ return [... s ].sort ()
172168})
173169
174170const uniqueInstruments = computed (() => {
175- const vals = new Set ()
176- for (const ds of props .datasets ) {
177- if (ds .instrument ) vals .add (ds .instrument )
178- }
179- return [... vals].sort ((a , b ) => a .localeCompare (b))
171+ const s = new Set ()
172+ props .datasets .forEach (d => { if (d .instrument ) s .add (d .instrument ) })
173+ return [... s].sort ()
180174})
181175
182- const filteredDatasets = computed (() => {
183- let result = props .datasets
176+ // Final rows: filter → search → sort
177+ const rows = computed (() => {
178+ let arr = props .datasets .slice ()
184179
185- // 1. Apply column filters first (MSNet only)
186- if (isMsnet .value ) {
187- if (filterSpecies .value ) {
188- result = result .filter (ds => {
189- const s = ds .species || (ds .organisms && ds .organisms [0 ]) || ' '
190- return s === filterSpecies .value
191- })
192- }
193- if (filterInstrument .value ) {
194- result = result .filter (ds => ds .instrument === filterInstrument .value )
195- }
180+ // Column filters
181+ if (isMsnet .value && filterSpecies .value ) {
182+ arr = arr .filter (d => (d .species || (d .organisms && d .organisms [0 ]) || ' ' ) === filterSpecies .value )
183+ }
184+ if (isMsnet .value && filterInstrument .value ) {
185+ arr = arr .filter (d => d .instrument === filterInstrument .value )
196186 }
197187
198- // 2. Apply text search
188+ // Text search
199189 const q = searchQuery .value .toLowerCase ().trim ()
200190 if (q) {
201- result = result .filter (ds =>
202- (ds .accession || ' ' ).toLowerCase ().includes (q) ||
203- (ds .title || ' ' ).toLowerCase ().includes (q) ||
204- (ds .species || ' ' ).toLowerCase ().includes (q) ||
205- (ds .instrument || ' ' ).toLowerCase ().includes (q)
191+ arr = arr .filter (d =>
192+ (d .accession || ' ' ).toLowerCase ().includes (q) ||
193+ (d .title || ' ' ).toLowerCase ().includes (q) ||
194+ (d .species || ' ' ).toLowerCase ().includes (q)
206195 )
207196 }
208197
209- return result
210- })
198+ // Sort
199+ const k = sKey .value
200+ if (! k) return arr
211201
212- const sortedDatasets = computed (() => {
213- const arr = [... filteredDatasets .value ]
214- if (! sortKey .value ) return arr // no sort active → original order
202+ const asc = sAsc .value
203+ const numeric = NUM .has (k)
215204
216- const key = sortKey . value
217- const dir = sortDir . value
218- const isNum = NUMERIC_COLS . has (key)
205+ return arr . slice (). sort (( a , b ) => {
206+ const va = a[k]
207+ const vb = b[k]
219208
220- arr .sort ((a , b ) => {
221- let av = a[key]
222- let bv = b[key]
223-
224- if (isNum) {
225- // Convert to number; treat null/undefined/empty as -Infinity so they go last
226- const na = (av != null && av !== ' ' ) ? Number (av) : null
227- const nb = (bv != null && bv !== ' ' ) ? Number (bv) : null
228- // Push nulls to bottom regardless of direction
229- if (na == null && nb == null ) return 0
230- if (na == null ) return 1
231- if (nb == null ) return - 1
232- return (na - nb) * dir
209+ if (numeric) {
210+ const na = typeof va === ' number' ? va : (va ? parseFloat (va) : NaN )
211+ const nb = typeof vb === ' number' ? vb : (vb ? parseFloat (vb) : NaN )
212+ const aOk = ! isNaN (na) && na > 0
213+ const bOk = ! isNaN (nb) && nb > 0
214+ if (! aOk && ! bOk) return 0
215+ if (! aOk) return 1 // empty always last
216+ if (! bOk) return - 1
217+ return asc ? na - nb : nb - na
233218 }
234219
235- // String sort
236- const sa = String (av ?? ' ' )
237- const sb = String (bv ?? ' ' )
238- return sa .localeCompare (sb) * dir
220+ const sa = String (va || ' ' )
221+ const sb = String (vb || ' ' )
222+ return asc ? sa .localeCompare (sb) : sb .localeCompare (sa)
239223 })
240- return arr
241224})
242225
243- function sortBy (key ) {
244- if (sortKey .value === key) {
245- // Toggle: desc → asc → off
246- if (sortDir .value === - 1 ) {
247- sortDir .value = 1
226+ function clickSort (key ) {
227+ if (sKey .value === key) {
228+ if (sAsc .value ) {
229+ sAsc .value = false // was asc → now desc
248230 } else {
249- // Reset sort
250- sortKey .value = ' '
251- sortDir .value = - 1
231+ sKey .value = ' ' // was desc → clear
232+ sAsc .value = true
252233 }
253234 } else {
254- // New column: start descending (biggest first for numbers)
255- sortKey . value = key
256- sortDir .value = NUMERIC_COLS .has (key) ? - 1 : 1
235+ sKey . value = key
236+ // Numbers start descending (biggest first), text starts ascending
237+ sAsc .value = ! NUM .has (key)
257238 }
258239}
259240
260- function sortIcon (key ) {
261- if (sortKey .value !== key) return ' ↕'
262- return sortDir .value === - 1 ? ' ↓ ' : ' ↑ '
241+ function icon (key ) {
242+ if (sKey .value !== key) return ' ↕'
243+ return sAsc .value ? ' ↑ ' : ' ↓ '
263244}
264245
265- function formatNum (n ) {
266- if (n === null || n === undefined || n === 0 ) return ' —'
246+ function fmtNum (n ) {
247+ if (n == null || n === 0 || n === ' ' ) return ' —'
267248 return Number (n).toLocaleString ()
268249}
269250
@@ -273,30 +254,33 @@ function navigateTo(ds) {
273254 </script >
274255
275256<style scoped>
276- .sort - icon {
277- font- size: 11px ;
278- opacity: 0.5 ;
279- margin- left: 2px ;
257+ .sortable {
258+ cursor : pointer ;
259+ user-select : none ;
260+ white-space : nowrap ;
261+ }
262+ .sortable :hover {
263+ color : var (--indigo , #6366f1 );
264+ }
265+ .num {
266+ text-align : right ;
280267}
281-
282268.filter-row th {
283269 padding : 4px 8px ;
284- background: var ( -- bg - secondary, #f5f5f5 );
270+ background : rgba ( 0 , 0 , 0 , 0.02 );
285271}
286-
287272.col-filter {
288273 width : 100% ;
289- font- size: 12px ;
290- padding: 2px 4px ;
291- border: 1px solid var (-- border- color , #ddd );
274+ font-size : 11 px ;
275+ padding : 3 px 4px ;
276+ border : 1px solid var (--border , #e2e8f0 );
292277 border-radius : 4px ;
293- background: var ( -- bg - primary, #fff) ;
278+ background : #fff ;
294279 color : var (--text-primary , #333 );
295280 cursor : pointer ;
296281 outline : none ;
297282}
298-
299283.col-filter :focus {
300- border- color: var (-- accent - color , #4a90e2 );
284+ border-color : var (--indigo , #6366f1 );
301285}
302286 </style >
0 commit comments