feat(frontend): add sorting modal for list view
This commit is contained in:
parent
1d02e76914
commit
d9ae9a2d2e
|
|
@ -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>
|
||||
|
|
@ -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)[]>([])
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue