multiple filter
Gitea Actions / Deploy (push) Successful in 30s

This commit is contained in:
vadim committed 2026-10-18 19:55:48 +03:00
1 parent 2d4b8ea9cf
commit 6af9993c4f
3 files changed
+72 -53

No files matched your search

+6 -8
View File
@@ -3,19 +3,18 @@ import { computed } from 'vue'
import AssigneeFilter from '@/components/AssigneeFilter.vue' import AssigneeFilter from '@/components/AssigneeFilter.vue'
import TextFilter from '@/components/TextFilter.vue' import TextFilter from '@/components/TextFilter.vue'
import StatusFilter from '@/components/StatusFilter.vue' import StatusFilter from '@/components/StatusFilter.vue'
import ProjectMenu from '@/components/ProjectMenu.vue'
interface Props { interface Props {
text?: string | null text?: string | null
assigneeId?: string | null assigneeId?: string | null
status?: string | null status?: string[]
} }
const props = withDefaults(defineProps<Props>(), { text: null, assigneeId: null }) const props = withDefaults(defineProps<Props>(), { text: null, assigneeId: null, status: () => [] })
const emit = defineEmits<{ const emit = defineEmits<{
'update:text': [value: string | null] 'update:text': [value: string | null]
'update:assigneeId': [value: string | null] 'update:assigneeId': [value: string | null]
'update:status': [value: string | null] 'update:status': [value: string[]]
'update:clear': [] 'update:clear': []
}>() }>()
@@ -25,14 +24,13 @@ function onAssigneeChanged(value: string[]) {
emit('update:assigneeId', value.at(-1) ?? null) emit('update:assigneeId', value.at(-1) ?? null)
} }
function onStatusChanged(alias: string) { function onStatusChanged(value: string[]) {
emit('update:status', alias) emit('update:status', value)
} }
</script> </script>
<template> <template>
<div class="filter-grid ga-4 align-center"> <div class="filter-grid ga-4 align-center">
<project-menu />
<text-filter :model-value="text" @update:model-value="emit('update:text', $event)" /> <text-filter :model-value="text" @update:model-value="emit('update:text', $event)" />
<status-filter :status="status" @changed="onStatusChanged" /> <status-filter :status="status" @changed="onStatusChanged" />
<assignee-filter :selected="selectedAssignees" @changed="onAssigneeChanged" /> <assignee-filter :selected="selectedAssignees" @changed="onAssigneeChanged" />
@@ -43,6 +41,6 @@ function onStatusChanged(alias: string) {
<style scoped lang="scss"> <style scoped lang="scss">
.filter-grid { .filter-grid {
display: grid; display: grid;
grid-template-columns: auto 1fr auto auto auto; grid-template-columns: 1fr auto auto auto;
} }
</style> </style>
+13 -6
View File
@@ -3,19 +3,25 @@ import { useStatusStore } from '@/stores/status.ts'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
interface Props { interface Props {
status?: string | null status?: string[]
} }
const { status } = defineProps<Props>() const { status = [] } = defineProps<Props>()
const emit = defineEmits<{ changed: [value: string] }>() const emit = defineEmits<{ changed: [value: string[]] }>()
const statusStore = useStatusStore() const statusStore = useStatusStore()
const selected = ref(status) const selected = ref<string[]>([...status])
const onUpdate = (val: unknown) => emit('changed', val as string)
// Keep store order so the same filter always yields the same URL
const onUpdate = (val: unknown) => {
if (!Array.isArray(val)) return
const order = statusStore.statuses?.map((el) => el.alias) ?? []
emit('changed', [...val].sort((a, b) => order.indexOf(a) - order.indexOf(b)))
}
watch( watch(
() => status, () => status,
(val) => { (val) => {
selected.value = val selected.value = [...val]
}, },
) )
</script> </script>
@@ -24,6 +30,7 @@ watch(
<v-btn-toggle <v-btn-toggle
v-model="selected" v-model="selected"
divided divided
multiple
border border
size="default" size="default"
density="comfortable" density="comfortable"
+53 -39
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue' import { computed, ref, useTemplateRef, watch } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useIssueStore } from '@/stores/issue' import { useIssueStore } from '@/stores/issue'
import { useProjectStore } from '@/stores/project.ts' import { useProjectStore } from '@/stores/project.ts'
@@ -30,7 +30,7 @@ const serverItems = ref<Issue[]>([])
const text = ref<string | null>(null) const text = ref<string | null>(null)
const assigneeId = ref<string | null>(null) const assigneeId = ref<string | null>(null)
const status = ref<string | null>(null) const status = ref<string[]>([])
const selected = ref<[string] | []>([]) const selected = ref<[string] | []>([])
const batch = ref<number[]>([]) const batch = ref<number[]>([])
const deleting = ref(false) const deleting = ref(false)
@@ -50,84 +50,98 @@ async function fetch() {
totalItems.value = res.meta.totalCount totalItems.value = res.meta.totalCount
} }
async function load() { // The URL is the source of truth: handlers only write the query, this watcher loads the list
syncQuery()
await fetch()
}
function syncQuery() { function syncQuery() {
const query: Record<string, string> = { page: String(page.value) } const query: Record<string, string> = { page: String(page.value) }
if (text.value) query.text = text.value if (text.value) query.text = text.value
if (assigneeId.value) query.assignee = assigneeId.value if (assigneeId.value) query.assignee = assigneeId.value
if (status.value) query.status = status.value if (status.value.length) query.status = status.value.join(',')
if (selected.value.length) query.selected_issue = selected.value[0] if (selected.value.length) query.selected_issue = selected.value[0]
router.replace({ query }) router.replace({ query })
} }
watch( watch(
() => route.query, [
(query) => { () => route.query.page,
page.value = query.page ? Number(query.page) : 1 () => route.query.text,
text.value = query.text ? String(query.text) : null () => route.query.assignee,
assigneeId.value = query.assignee ? String(query.assignee) : null () => route.query.status,
status.value = query.status ? String(query.status) : null currentProjectId,
selected.value = query.selected_issue ? [String(query.selected_issue)] : [] ],
async ([qPage, qText, qAssignee, qStatus, projectId], old) => {
// The route changes before this view unmounts when navigating away
if (route.name !== 'issues') return
page.value = qPage ? Number(qPage) : 1
text.value = qText ? String(qText) : null
assigneeId.value = qAssignee ? String(qAssignee) : null
status.value = qStatus ? String(qStatus).split(',') : []
if (old && projectId !== old[4]) {
clearBatch()
close()
// Resetting the page changes the query and re-runs this watcher
if (page.value !== 1) {
page.value = 1
syncQuery()
return
}
}
await fetch()
},
{ immediate: true },
)
watch(
() => route.query.selected_issue,
(key) => {
if (route.name !== 'issues') return
selected.value = key ? [String(key)] : []
}, },
{ immediate: true }, { immediate: true },
) )
watch(selected, () => syncQuery()) watch(selected, () => syncQuery())
watch(currentProjectId, async () => { watch(currentProjectId, (projectId) => issueStore.getStat(projectId), { immediate: true })
page.value = 1
clearBatch()
close()
await load()
await issueStore.getStat(currentProjectId.value)
})
onMounted(async () => {
await load()
await issueStore.getStat(currentProjectId.value)
})
const splitContainer = useTemplateRef<HTMLElement>('splitContainer') const splitContainer = useTemplateRef<HTMLElement>('splitContainer')
const split = useSplitPane(splitContainer, { storageKey: 'issues-list', min: 0.25, max: 0.75 }) const split = useSplitPane(splitContainer, { storageKey: 'issues-list', min: 0.25, max: 0.75 })
const gridTemplate = computed(() => (selected.value.length ? split.gridTemplate.value : '1fr')) const gridTemplate = computed(() => (selected.value.length ? split.gridTemplate.value : '1fr'))
const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`) const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`)
async function updatePage(val: number) { function updatePage(val: number) {
page.value = val page.value = val
await load() syncQuery()
} }
async function onTextChanged(value: string | null) { function onTextChanged(value: string | null) {
text.value = value text.value = value
page.value = 1 page.value = 1
await load() syncQuery()
} }
async function onAssigneeChanged(value: string | null) { function onAssigneeChanged(value: string | null) {
assigneeId.value = value assigneeId.value = value
page.value = 1 page.value = 1
await load() syncQuery()
} }
async function onStatusChanged(value: string | null) { function onStatusChanged(value: string[]) {
status.value = value status.value = value
page.value = 1 page.value = 1
await load() syncQuery()
} }
async function onClear() { function onClear() {
text.value = null text.value = null
assigneeId.value = null assigneeId.value = null
status.value = null status.value = []
page.value = 1 page.value = 1
syncQuery()
await load()
} }
function close() { function close() {