mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-03-09 07:13:35 -05:00
refactor(sort): replace modal with inline popup and improve UX
- Switch from Modal to lightweight Popup component - Hide sort order picker when Manually (position) is selected - Sort dropdown options alphabetically, keeping Manually first - Add description text explaining sort behavior
This commit is contained in:
@@ -1,69 +1,74 @@
|
||||
<template>
|
||||
<XButton
|
||||
variant="secondary"
|
||||
icon="sort"
|
||||
@click="() => modalOpen = true"
|
||||
>
|
||||
{{ $t('project.list.sort') }}
|
||||
</XButton>
|
||||
<Modal
|
||||
:enabled="modalOpen"
|
||||
transition-name="fade"
|
||||
variant="hint-modal"
|
||||
@close="() => modalOpen = false"
|
||||
>
|
||||
<div class="sort-popup">
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('sorting.sortBy') }}</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="sortField">
|
||||
<option
|
||||
v-for="o in options"
|
||||
:key="o.value"
|
||||
:value="o.value"
|
||||
>
|
||||
{{ o.label }}
|
||||
</option>
|
||||
</select>
|
||||
<Popup>
|
||||
<template #trigger="{toggle}">
|
||||
<XButton
|
||||
variant="secondary"
|
||||
icon="sort"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ $t('project.list.sort') }}
|
||||
</XButton>
|
||||
</template>
|
||||
<template #content="{close}">
|
||||
<Card class="sort-popup">
|
||||
<p class="sort-description has-text-grey is-size-7">
|
||||
{{ $t('sorting.description') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('sorting.sortBy') }}</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="sortField">
|
||||
<option
|
||||
v-for="o in options"
|
||||
:key="o.value"
|
||||
:value="o.value"
|
||||
>
|
||||
{{ o.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('sorting.order') }}</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="sortOrder">
|
||||
<option value="asc">
|
||||
{{ $t('sorting.asc') }}
|
||||
</option>
|
||||
<option value="desc">
|
||||
{{ $t('sorting.desc') }}
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
v-if="!isManualSort"
|
||||
class="field"
|
||||
>
|
||||
<label class="label">{{ $t('sorting.order') }}</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="sortOrder">
|
||||
<option value="asc">
|
||||
{{ $t('sorting.asc') }}
|
||||
</option>
|
||||
<option value="desc">
|
||||
{{ $t('sorting.desc') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<XButton
|
||||
variant="tertiary"
|
||||
class="has-text-danger"
|
||||
@click="modalOpen = false"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
variant="primary"
|
||||
@click="applySort"
|
||||
>
|
||||
{{ $t('misc.doit') }}
|
||||
</XButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div class="actions">
|
||||
<XButton
|
||||
variant="tertiary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
variant="primary"
|
||||
@click="applySort(close)"
|
||||
>
|
||||
{{ $t('misc.doit') }}
|
||||
</XButton>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
</Popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from 'vue'
|
||||
import {ref, computed, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import XButton from '@/components/input/Button.vue'
|
||||
import Modal from '@/components/misc/Modal.vue'
|
||||
import Popup from '@/components/misc/Popup.vue'
|
||||
import Card from '@/components/misc/Card.vue'
|
||||
import type {SortBy} from '@/composables/useTaskList'
|
||||
|
||||
const props = defineProps<{ modelValue: SortBy }>()
|
||||
@@ -71,50 +76,64 @@ const emit = defineEmits<{ 'update:modelValue': [value: SortBy] }>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const sortField = ref<string>('position')
|
||||
const sortOrder = ref<'asc' | 'desc'>('asc')
|
||||
|
||||
const isManualSort = computed(() => sortField.value === 'position')
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
const key = Object.keys(val)[0] || 'position'
|
||||
sortField.value = key
|
||||
sortOrder.value = (val as SortBy)[key as keyof SortBy] ?? 'asc'
|
||||
}, {immediate: true})
|
||||
|
||||
const options = [
|
||||
{value: 'position', label: t('sorting.position')},
|
||||
{value: 'title', label: t('task.attributes.title')},
|
||||
{value: 'priority', label: t('task.attributes.priority')},
|
||||
{value: 'due_date', label: t('task.attributes.dueDate')},
|
||||
{value: 'start_date', label: t('task.attributes.startDate')},
|
||||
{value: 'end_date', label: t('task.attributes.endDate')},
|
||||
{value: 'percent_done', label: t('task.attributes.percentDone')},
|
||||
{value: 'created', label: t('task.attributes.created')},
|
||||
{value: 'updated', label: t('task.attributes.updated')},
|
||||
]
|
||||
const options = computed(() => {
|
||||
const manualOption = {value: 'position', label: t('sorting.manually')}
|
||||
const otherOptions = [
|
||||
{value: 'title', label: t('task.attributes.title')},
|
||||
{value: 'priority', label: t('task.attributes.priority')},
|
||||
{value: 'due_date', label: t('task.attributes.dueDate')},
|
||||
{value: 'start_date', label: t('task.attributes.startDate')},
|
||||
{value: 'end_date', label: t('task.attributes.endDate')},
|
||||
{value: 'percent_done', label: t('task.attributes.percentDone')},
|
||||
{value: 'created', label: t('task.attributes.created')},
|
||||
{value: 'updated', label: t('task.attributes.updated')},
|
||||
].sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
function applySort() {
|
||||
return [manualOption, ...otherOptions]
|
||||
})
|
||||
|
||||
function applySort(close: () => void) {
|
||||
const sort: SortBy = {} as SortBy
|
||||
;(sort as Record<string, 'asc' | 'desc'>)[sortField.value] = sortOrder.value
|
||||
const order = isManualSort.value ? 'asc' : sortOrder.value
|
||||
;(sort as Record<string, 'asc' | 'desc'>)[sortField.value] = order
|
||||
emit('update:modelValue', sort)
|
||||
modalOpen.value = false
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sort-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin: 0;
|
||||
min-inline-size: 18rem;
|
||||
|
||||
.field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
:deep(.card-content .content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
}
|
||||
.sort-description {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user