feat(frontend): add sorting modal for list view

This commit is contained in:
kolaente 2025-06-25 16:09:32 +02:00
parent 1d02e76914
commit d9ae9a2d2e
3 changed files with 150 additions and 23 deletions

View File

@ -0,0 +1,120 @@
<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>
</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>
</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>
</template>
<script setup lang="ts">
import {ref, watch} from 'vue'
import {useI18n} from 'vue-i18n'
import XButton from '@/components/base/XButton.vue'
import Modal from '@/components/misc/Modal.vue'
import type {SortBy} from '@/composables/useTaskList'
const props = defineProps<{ modelValue: SortBy }>()
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')
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')},
]
function applySort() {
const sort: SortBy = {} as SortBy
;(sort as Record<string, 'asc' | 'desc'>)[sortField.value] = sortOrder.value
emit('update:modelValue', sort)
modalOpen.value = false
}
</script>
<style scoped lang="scss">
.sort-popup {
display: flex;
flex-direction: column;
align-items: stretch;
.field {
margin-bottom: 1rem;
}
.actions {
display: flex;
justify-content: flex-end;
gap: .5rem;
}
}
</style>

View File

@ -14,6 +14,9 @@
:project-id="projectId"
@update:modelValue="prepareFiltersAndLoadTasks()"
/>
<SortPopup
v-model="sortByParam"
/>
</div>
</template>
@ -49,13 +52,13 @@
v-if="tasks && tasks.length > 0"
v-model="tasks"
:group="{name: 'tasks', put: false}"
:disabled="!canDragTasks"
:disabled="!canDragTasks || !isPositionSorting"
item-key="id"
tag="ul"
:component-data="{
class: {
tasks: true,
'dragging-disabled': !canDragTasks || isAlphabeticalSorting
'dragging-disabled': !canDragTasks || !isPositionSorting
},
type: 'transition-group'
}"
@ -71,14 +74,14 @@
<SingleTaskInProject
:ref="(el) => setTaskRef(el, index)"
:show-list-color="false"
:disabled="!canDragTasks"
:disabled="!canDragTasks || !isPositionSorting"
:can-mark-as-done="canWrite || isPseudoProject"
:the-task="t"
:all-tasks="allTasks"
@taskUpdated="updateTasks"
>
<span
v-if="canDragTasks"
v-if="canDragTasks && isPositionSorting"
class="icon handle"
>
<Icon icon="grip-lines" />
@ -109,7 +112,7 @@ import SingleTaskInProject from '@/components/tasks/partials/SingleTaskInProject
import FilterPopup from '@/components/project/partials/FilterPopup.vue'
import Nothing from '@/components/misc/Nothing.vue'
import Pagination from '@/components/misc/Pagination.vue'
import {ALPHABETICAL_SORT} from '@/components/project/partials/Filters.vue'
import SortPopup from '@/components/project/partials/SortPopup.vue'
import {useTaskList} from '@/composables/useTaskList'
import {useTaskDragToProject} from '@/composables/useTaskDragToProject'
@ -171,8 +174,8 @@ watch(
},
)
const isAlphabeticalSorting = computed(() => {
return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
const isPositionSorting = computed(() => {
return Object.keys(sortByParam.value).length === 0 || (Object.keys(sortByParam.value).length === 1 && typeof sortByParam.value.position !== 'undefined')
})
const firstNewPosition = computed(() => {
@ -214,15 +217,15 @@ function focusNewTaskInput() {
}
function updateTaskList(task: ITask) {
if (isAlphabeticalSorting.value) {
// reload tasks with current filter and sorting
loadTasks()
} else {
allTasks.value = [
task,
...allTasks.value,
]
}
if (!isPositionSorting.value) {
// reload tasks with current filter and sorting
loadTasks()
} else {
allTasks.value = [
task,
...allTasks.value,
]
}
baseStore.setHasTasks(true)
}
@ -287,12 +290,7 @@ async function saveTaskPosition(e: { originalEvent?: MouseEvent, to: HTMLElement
}
function prepareFiltersAndLoadTasks() {
if (isAlphabeticalSorting.value) {
sortByParam.value = {}
sortByParam.value[ALPHABETICAL_SORT] = 'asc'
}
loadTasks()
loadTasks()
}
const taskRefs = ref<(InstanceType<typeof SingleTaskInProject> | null)[]>([])

View File

@ -380,7 +380,8 @@
"addPlaceholder": "Add a task…",
"empty": "This project is currently empty.",
"newTaskCta": "Create a task.",
"editTask": "Edit Task"
"editTask": "Edit Task",
"sort": "Sort"
},
"gantt": {
"title": "Gantt",
@ -572,6 +573,14 @@
}
}
},
"sorting": {
"title": "Sort Tasks",
"sortBy": "Sort by",
"order": "Order",
"position": "Position",
"asc": "Ascending",
"desc": "Descending"
},
"migrate": {
"title": "Import from other services",
"titleService": "Import your data from {name} into Vikunja",