feat(sort): add sorting popup for list view
This commit is contained in:
parent
b20df2ef63
commit
408e5b347c
|
|
@ -55,10 +55,6 @@
|
|||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export const ALPHABETICAL_SORT = 'title'
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<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">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="selected">
|
||||
<option
|
||||
v-for="o in options"
|
||||
:key="o.value"
|
||||
:value="o.value"
|
||||
>
|
||||
{{ o.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<XButton
|
||||
variant="tertiary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
variant="primary"
|
||||
@click="applySort(close)"
|
||||
>
|
||||
{{ $t('sorting.apply') }}
|
||||
</XButton>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
</Popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import XButton from '@/components/input/Button.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 }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: SortBy] }>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const MANUAL = 'position:asc'
|
||||
const selected = ref<string>(MANUAL)
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
const key = Object.keys(val)[0]
|
||||
if (!key || key === 'position') {
|
||||
selected.value = MANUAL
|
||||
return
|
||||
}
|
||||
const order = (val as Record<string, 'asc' | 'desc'>)[key] ?? 'asc'
|
||||
selected.value = `${key}:${order}`
|
||||
}, {immediate: true})
|
||||
|
||||
const options = computed(() => {
|
||||
const manual = {value: MANUAL, label: t('sorting.manually')}
|
||||
const rest = [
|
||||
{value: 'title:asc', label: t('sorting.options.titleAsc')},
|
||||
{value: 'title:desc', label: t('sorting.options.titleDesc')},
|
||||
{value: 'priority:desc', label: t('sorting.options.priorityDesc')},
|
||||
{value: 'priority:asc', label: t('sorting.options.priorityAsc')},
|
||||
{value: 'due_date:asc', label: t('sorting.options.dueDateAsc')},
|
||||
{value: 'due_date:desc', label: t('sorting.options.dueDateDesc')},
|
||||
{value: 'start_date:asc', label: t('sorting.options.startDateAsc')},
|
||||
{value: 'start_date:desc', label: t('sorting.options.startDateDesc')},
|
||||
{value: 'end_date:asc', label: t('sorting.options.endDateAsc')},
|
||||
{value: 'end_date:desc', label: t('sorting.options.endDateDesc')},
|
||||
{value: 'percent_done:desc', label: t('sorting.options.percentDoneDesc')},
|
||||
{value: 'percent_done:asc', label: t('sorting.options.percentDoneAsc')},
|
||||
{value: 'created:desc', label: t('sorting.options.createdDesc')},
|
||||
{value: 'created:asc', label: t('sorting.options.createdAsc')},
|
||||
{value: 'updated:desc', label: t('sorting.options.updatedDesc')},
|
||||
{value: 'updated:asc', label: t('sorting.options.updatedAsc')},
|
||||
].sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
return [manual, ...rest]
|
||||
})
|
||||
|
||||
function applySort(close: () => void) {
|
||||
const [field, order] = selected.value.split(':') as [string, 'asc' | 'desc']
|
||||
const sort: SortBy = {} as SortBy
|
||||
;(sort as Record<string, 'asc' | 'desc'>)[field] = order
|
||||
emit('update:modelValue', sort)
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sort-popup {
|
||||
margin: 0;
|
||||
min-inline-size: 18rem;
|
||||
|
||||
:deep(.card-content .content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sort-description {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,12 +7,15 @@
|
|||
>
|
||||
<template #header>
|
||||
<div class="filter-container">
|
||||
<SortPopup
|
||||
v-model="sortByParam"
|
||||
/>
|
||||
<FilterPopup
|
||||
v-if="!isSavedFilter(project)"
|
||||
v-model="params"
|
||||
:view-id="viewId"
|
||||
:project-id="projectId"
|
||||
@update:modelValue="prepareFiltersAndLoadTasks()"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</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,13 @@
|
|||
<SingleTaskInProject
|
||||
:ref="(el) => setTaskRef(el, index)"
|
||||
:show-list-color="false"
|
||||
:disabled="!canDragTasks"
|
||||
: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 +111,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'
|
||||
|
|
@ -172,9 +174,7 @@ watch(
|
|||
},
|
||||
)
|
||||
|
||||
const isAlphabeticalSorting = computed(() => {
|
||||
return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
|
||||
})
|
||||
const isPositionSorting = computed(() => 'position' in sortByParam.value)
|
||||
|
||||
const firstNewPosition = computed(() => {
|
||||
if (tasks.value.length === 0) {
|
||||
|
|
@ -215,7 +215,7 @@ function focusNewTaskInput() {
|
|||
}
|
||||
|
||||
function updateTaskList(task: ITask) {
|
||||
if (isAlphabeticalSorting.value) {
|
||||
if (!isPositionSorting.value) {
|
||||
// reload tasks with current filter and sorting
|
||||
loadTasks()
|
||||
} else {
|
||||
|
|
@ -287,15 +287,6 @@ async function saveTaskPosition(e: { originalEvent?: MouseEvent, to: HTMLElement
|
|||
}
|
||||
}
|
||||
|
||||
function prepareFiltersAndLoadTasks() {
|
||||
if (isAlphabeticalSorting.value) {
|
||||
sortByParam.value = {}
|
||||
sortByParam.value[ALPHABETICAL_SORT] = 'asc'
|
||||
}
|
||||
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
const taskRefs = ref<(InstanceType<typeof SingleTaskInProject> | null)[]>([])
|
||||
const focusedIndex = ref(-1)
|
||||
|
||||
|
|
@ -365,6 +356,12 @@ onBeforeUnmount(() => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,7 +434,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",
|
||||
|
|
@ -626,6 +627,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sorting": {
|
||||
"manually": "Manually",
|
||||
"apply": "Apply sort",
|
||||
"description": "Choose how tasks in this list are sorted. When sorting manually, you can drag and drop tasks to reorder them.",
|
||||
"options": {
|
||||
"titleAsc": "Title (A–Z)",
|
||||
"titleDesc": "Title (Z–A)",
|
||||
"priorityDesc": "Priority (Highest first)",
|
||||
"priorityAsc": "Priority (Lowest first)",
|
||||
"dueDateAsc": "Due date (Earliest first)",
|
||||
"dueDateDesc": "Due date (Latest first)",
|
||||
"startDateAsc": "Start date (Earliest first)",
|
||||
"startDateDesc": "Start date (Latest first)",
|
||||
"endDateAsc": "End date (Earliest first)",
|
||||
"endDateDesc": "End date (Latest first)",
|
||||
"percentDoneDesc": "% done (Most done first)",
|
||||
"percentDoneAsc": "% done (Least done first)",
|
||||
"createdDesc": "Created (Newest first)",
|
||||
"createdAsc": "Created (Oldest first)",
|
||||
"updatedDesc": "Updated (Newest first)",
|
||||
"updatedAsc": "Updated (Oldest first)"
|
||||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
|
|
|
|||
Loading…
Reference in New Issue