Skip to content

Commit 62cf850

Browse files
ypriverolclaude
andcommitted
fix: complete rewrite of DatasetTable sorting
Clean rewrite from scratch: - Single 'rows' computed: filter → search → sort (no intermediate) - Sort uses parseFloat for numbers, localeCompare for strings - Empty/zero values always pushed to bottom - Click: numbers start descending, text starts ascending - 2nd click reverses, 3rd click clears sort - v-for key uses index to avoid duplicate accession issues - Simpler state: sKey + sAsc refs (no complex toggling) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5a4f701 commit 62cf850

1 file changed

Lines changed: 111 additions & 127 deletions

File tree

frontend/src/components/DatasetTable.vue

Lines changed: 111 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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);">
@@ -20,54 +20,53 @@
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>
@@ -76,14 +75,14 @@
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
>
@@ -113,14 +112,14 @@
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
143142
const router = useRouter()
144143
const searchQuery = ref('')
145-
const sortKey = ref('')
146-
const sortDir = ref(-1) // -1 = descending (biggest first, natural for counts)
147-
148-
// Column filters (MSNet only)
149144
const filterSpecies = ref('')
150145
const 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
158154
watch(() => 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)
165161
const 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
174170
const 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: 11px;
275+
padding: 3px 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

Comments
 (0)