feat(ui): collapse Downloads filters into compact bar with popover

Replaces two stacked filter rows (4 fields + superseded switch) with a
single "Filters" button that opens a popover containing all controls.
Active filters surface as dismissable chips on the bar alongside a
"Clear all" button, so what's currently applied stays visible without
eating vertical space above the table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 15:10:29 -04:00
parent c5ebbe5a76
commit 0426e91a84
+139 -55
View File
@@ -68,62 +68,120 @@
</v-col> </v-col>
</v-row> </v-row>
<!-- Filters --> <!-- Filters (compact bar with popover) -->
<v-row class="mb-4"> <div class="d-flex align-center flex-wrap ga-2 mb-4">
<v-col cols="12" md="3"> <v-menu :close-on-content-click="false" location="bottom start">
<v-select <template v-slot:activator="{ props }">
v-model="filterStatus" <v-btn v-bind="props" variant="outlined" size="small" prepend-icon="mdi-filter-variant">
:items="statusOptions" Filters
label="Status" <v-chip v-if="activeFilterCount" class="ml-2" size="x-small" color="primary">
density="compact" {{ activeFilterCount }}
variant="outlined" </v-chip>
clearable <v-icon end>mdi-chevron-down</v-icon>
/> </v-btn>
</v-col> </template>
<v-col cols="12" md="3"> <v-card min-width="320" class="pa-4">
<v-select <v-select
v-model="filterSourceId" v-model="filterStatus"
:items="sourceOptions" :items="statusOptions"
label="Source" label="Status"
density="compact" density="compact"
variant="outlined" variant="outlined"
clearable clearable
/> class="mb-2"
</v-col> hide-details
<v-col cols="12" md="3"> />
<v-text-field <v-select
v-model="filterFromDate" v-model="filterSourceId"
label="From Date" :items="sourceOptions"
type="date" label="Source"
density="compact" density="compact"
variant="outlined" variant="outlined"
clearable clearable
/> class="mb-2"
</v-col> hide-details
<v-col cols="12" md="3"> />
<v-text-field <v-text-field
v-model="filterToDate" v-model="filterFromDate"
label="To Date" label="From Date"
type="date" type="date"
density="compact" density="compact"
variant="outlined" variant="outlined"
clearable clearable
/> class="mb-2"
</v-col> hide-details
</v-row> />
<v-text-field
v-model="filterToDate"
label="To Date"
type="date"
density="compact"
variant="outlined"
clearable
class="mb-3"
hide-details
/>
<v-switch
v-model="filterExcludeSuperseded"
label="Hide superseded failures"
density="compact"
hide-details
color="primary"
/>
</v-card>
</v-menu>
<!-- Superseded filter --> <!-- Active filter chips -->
<v-row class="mb-4"> <v-chip
<v-col cols="12"> v-if="filterStatus"
<v-switch size="small"
v-model="filterExcludeSuperseded" closable
label="Hide superseded failures" @click:close="filterStatus = null"
density="compact" >
hide-details Status: {{ statusLabel(filterStatus) }}
color="primary" </v-chip>
/> <v-chip
</v-col> v-if="filterSourceId"
</v-row> size="small"
closable
@click:close="filterSourceId = null"
>
Source: {{ sourceLabel(filterSourceId) }}
</v-chip>
<v-chip
v-if="filterFromDate"
size="small"
closable
@click:close="filterFromDate = null"
>
From: {{ filterFromDate }}
</v-chip>
<v-chip
v-if="filterToDate"
size="small"
closable
@click:close="filterToDate = null"
>
To: {{ filterToDate }}
</v-chip>
<v-chip
v-if="!filterExcludeSuperseded"
size="small"
closable
@click:close="filterExcludeSuperseded = true"
>
Showing superseded
</v-chip>
<v-btn
v-if="activeFilterCount"
size="small"
variant="text"
@click="clearAllFilters"
>
Clear all
</v-btn>
</div>
<!-- Downloads Table --> <!-- Downloads Table -->
<v-card> <v-card>
@@ -334,6 +392,32 @@ const sourceOptions = computed(() =>
})) }))
) )
const activeFilterCount = computed(() => {
let n = 0
if (filterStatus.value) n++
if (filterSourceId.value) n++
if (filterFromDate.value) n++
if (filterToDate.value) n++
if (!filterExcludeSuperseded.value) n++
return n
})
function statusLabel(value) {
return statusOptions.find(s => s.value === value)?.title || value
}
function sourceLabel(id) {
return sourceOptions.value.find(s => s.value === id)?.title || `#${id}`
}
function clearAllFilters() {
filterStatus.value = null
filterSourceId.value = null
filterFromDate.value = null
filterToDate.value = null
filterExcludeSuperseded.value = true
}
const headers = [ const headers = [
{ title: 'Status', key: 'status', width: 120 }, { title: 'Status', key: 'status', width: 120 },
{ title: 'Source', key: 'source', width: 180 }, { title: 'Source', key: 'source', width: 180 },