feat: ListList script setup (#2441)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2441
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
Dominik Pschenitschni 2022-09-28 16:08:23 +00:00 committed by konrad
parent 63f2e6ba6f
commit bbf4ef4697
6 changed files with 163 additions and 177 deletions

View file

@ -1,6 +1,6 @@
<template> <template>
<dropdown> <dropdown>
<template v-if="isSavedFilter"> <template v-if="isSavedFilter(list)">
<dropdown-item <dropdown-item
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }" :to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
icon="pen" icon="pen"
@ -78,7 +78,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, watchEffect, type PropType} from 'vue' import {ref, computed, watchEffect, type PropType} from 'vue'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import TaskSubscription from '@/components/misc/subscription.vue'
@ -100,5 +100,4 @@ watchEffect(() => {
const configStore = useConfigStore() const configStore = useConfigStore()
const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders?.length > 0) const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders?.length > 0)
const isSavedFilter = computed(() => getSavedFilterIdFromListId(props.list.id) > 0)
</script> </script>

View file

@ -2,6 +2,7 @@ import {ref, shallowReactive, watch, computed} from 'vue'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
import type { ITask } from '@/modelTypes/ITask'
// FIXME: merge with DEFAULT_PARAMS in filters.vue // FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({ export const getDefaultParams = () => ({
@ -70,7 +71,7 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
const loading = computed(() => taskCollectionService.loading) const loading = computed(() => taskCollectionService.loading)
const totalPages = computed(() => taskCollectionService.totalPages) const totalPages = computed(() => taskCollectionService.totalPages)
const tasks = ref([]) const tasks = ref<ITask[]>([])
async function loadTasks() { async function loadTasks() {
tasks.value = [] tasks.value = []
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value) tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
@ -81,10 +82,10 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
watch(() => route.query, (query) => { watch(() => route.query, (query) => {
const { page: pageQueryValue, search: searchQuery } = query const { page: pageQueryValue, search: searchQuery } = query
if (searchQuery !== undefined) { if (searchQuery !== undefined) {
search.value = searchQuery search.value = searchQuery as string
} }
if (pageQueryValue !== undefined) { if (pageQueryValue !== undefined) {
page.value = parseInt(pageQueryValue) page.value = Number(pageQueryValue)
} }
}, { immediate: true }) }, { immediate: true })

View file

@ -7,4 +7,8 @@ export function getSavedFilterIdFromListId(listId: IList['id']) {
filterId = 0 filterId = 0
} }
return filterId return filterId
}
export function isSavedFilter(list: IList) {
return getSavedFilterIdFromListId(list.id) > 0
} }

View file

@ -9,8 +9,6 @@ import type {ITask} from '@/modelTypes/ITask'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
export default class ListModel extends AbstractModel<IList> implements IList { export default class ListModel extends AbstractModel<IList> implements IList {
id = 0 id = 0
title = '' title = ''
@ -52,12 +50,4 @@ export default class ListModel extends AbstractModel<IList> implements IList {
this.created = new Date(this.created) this.created = new Date(this.created)
this.updated = new Date(this.updated) this.updated = new Date(this.updated)
} }
isSavedFilter() {
return this.getSavedFilterId() > 0
}
getSavedFilterId() {
return getSavedFilterIdFromListId(this.id)
}
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban"> <ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
<template #header> <template #header>
<div class="filter-container" v-if="isSavedFilter"> <div class="filter-container" v-if="isSavedFilter(list)">
<div class="items"> <div class="items">
<filter-popup <filter-popup
v-model="params" v-model="params"
@ -239,6 +239,7 @@ import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveC
import {calculateItemPosition} from '../../helpers/calculateItemPosition' import {calculateItemPosition} from '../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card.vue' import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import {isSavedFilter} from '@/helpers/savedFilter'
const DRAG_OPTIONS = { const DRAG_OPTIONS = {
// sortable options // sortable options
@ -324,9 +325,6 @@ export default defineComponent({
}) })
}, },
isSavedFilter() {
return this.list.isSavedFilter && !this.list.isSavedFilter()
},
loadBucketParameter() { loadBucketParameter() {
return { return {
listId: this.listId, listId: this.listId,
@ -356,6 +354,8 @@ export default defineComponent({
}, },
methods: { methods: {
isSavedFilter,
loadBuckets() { loadBuckets() {
const {listId, params} = this.loadBucketParameter const {listId, params} = this.loadBucketParameter

View file

@ -3,7 +3,7 @@
<template #header> <template #header>
<div <div
class="filter-container" class="filter-container"
v-if="list.isSavedFilter && !list.isSavedFilter()" v-if="!isSavedFilter(list)"
> >
<div class="items"> <div class="items">
<div class="search"> <div class="search">
@ -58,7 +58,7 @@
> >
<add-task <add-task
@taskAdded="updateTaskList" @taskAdded="updateTaskList"
ref="addTask" ref="addTaskRef"
:default-position="firstNewPosition" :default-position="firstNewPosition"
/> />
</template> </template>
@ -76,7 +76,7 @@
v-if="tasks && tasks.length > 0" v-if="tasks && tasks.length > 0"
> >
<draggable <draggable
v-bind="dragOptions" v-bind="DRAG_OPTIONS"
v-model="tasks" v-model="tasks"
group="tasks" group="tasks"
@start="() => drag = true" @start="() => drag = true"
@ -94,7 +94,7 @@
<single-task-in-list <single-task-in-list
:show-list-color="false" :show-list-color="false"
:disabled="!canWrite" :disabled="!canWrite"
:can-mark-as-done="canWrite || (list.isSavedFilter && list.isSavedFilter())" :can-mark-as-done="canWrite || isSavedFilter(list)"
:the-task="t" :the-task="t"
@taskUpdated="updateTasks" @taskUpdated="updateTasks"
> >
@ -114,7 +114,7 @@
</template> </template>
</draggable> </draggable>
</div> </div>
<edit-task <EditTask
v-if="isTaskEdit" v-if="isTaskEdit"
class="taskedit mt-0" class="taskedit mt-0"
:title="$t('list.list.editTask')" :title="$t('list.list.editTask')"
@ -135,25 +135,32 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, toRef, defineComponent } from 'vue' export default { name: 'List' }
</script>
<script setup lang="ts">
import {ref, computed, toRef, nextTick, onMounted} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import {useRoute, useRouter} from 'vue-router'
import ListWrapper from './ListWrapper.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue' import ButtonLink from '@/components/misc/ButtonLink.vue'
import ListWrapper from './ListWrapper.vue'
import EditTask from '@/components/tasks/edit-task.vue' import EditTask from '@/components/tasks/edit-task.vue'
import AddTask from '@/components/tasks/add-task.vue' import AddTask from '@/components/tasks/add-task.vue'
import SingleTaskInList from '@/components/tasks/partials/singleTaskInList.vue' import SingleTaskInList from '@/components/tasks/partials/singleTaskInList.vue'
import { useTaskList } from '@/composables/taskList'
import {RIGHTS as Rights} from '@/constants/rights'
import FilterPopup from '@/components/list/partials/filter-popup.vue' import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue' import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue' import Pagination from '@/components/misc/pagination.vue'
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue' import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import draggable from 'zhyswan-vuedraggable' import {useStore} from '@/store'
import {calculateItemPosition} from '../../helpers/calculateItemPosition' import {HAS_TASKS} from '@/store/mutation-types'
import {useTaskList} from '@/composables/taskList'
import {RIGHTS as Rights} from '@/constants/rights'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {isSavedFilter} from '@/helpers/savedFilter'
function sortTasks(tasks: ITask[]) { function sortTasks(tasks: ITask[]) {
if (tasks === null || Array.isArray(tasks) && tasks.length === 0) { if (tasks === null || Array.isArray(tasks) && tasks.length === 0) {
@ -173,153 +180,138 @@ function sortTasks(tasks: ITask[]) {
}) })
} }
export default defineComponent({ const props = defineProps({
name: 'List', listId: {
type: Number,
props: { required: true,
listId: {
type: Number,
required: true,
},
},
data() {
return {
ctaVisible: false,
showTaskSearch: false,
drag: false,
dragOptions: {
animation: 100,
ghostClass: 'ghost',
},
}
},
components: {
BaseButton,
ListWrapper,
Nothing,
FilterPopup,
SingleTaskInList,
EditTask,
AddTask,
draggable,
Pagination,
ButtonLink,
},
setup(props) {
const taskEditTask = ref(null)
const isTaskEdit = ref(false)
// This function initializes the tasks page and loads the first page of tasks
// function beforeLoad() {
// taskEditTask.value = null
// isTaskEdit.value = false
// }
const taskList = useTaskList(toRef(props, 'listId'), {
position: 'asc',
})
return {
taskEditTask,
isTaskEdit,
...taskList,
}
},
computed: {
isAlphabeticalSorting() {
return this.params.sort_by.find( sortBy => sortBy === ALPHABETICAL_SORT ) !== undefined
},
firstNewPosition() {
if (this.tasks.length === 0) {
return 0
}
return calculateItemPosition(null, this.tasks[0].position)
},
canWrite() {
return this.list.maxRight > Rights.READ && this.list.id > 0
},
list() {
return this.$store.state.currentList
},
},
mounted() {
this.$nextTick(() => (this.ctaVisible = true))
},
methods: {
searchTasks() {
// Only search if the search term changed
if (this.$route.query === this.searchTerm) {
return
}
this.$router.push({
name: 'list.list',
query: {search: this.searchTerm},
})
},
hideSearchBar() {
// This is a workaround.
// When clicking on the search button, @blur from the input is fired. If we
// would then directly hide the whole search bar directly, no click event
// from the button gets fired. To prevent this, we wait 200ms until we hide
// everything so the button has a chance of firing the search event.
setTimeout(() => {
this.showTaskSearch = false
}, 200)
},
focusNewTaskInput() {
this.$refs.addTask.focusTaskInput()
},
updateTaskList(task: ITask) {
if ( this.isAlphabeticalSorting ) {
// reload tasks with current filter and sorting
this.loadTasks(1, undefined, undefined, true)
}
else {
this.tasks = [
task,
...this.tasks,
]
}
this.$store.commit(HAS_TASKS, true)
},
editTask(id: ITask['id']) {
this.taskEditTask = {...this.tasks.find(t => t.id === parseInt(id))}
this.isTaskEdit = true
},
updateTasks(updatedTask: ITask) {
for (const t in this.tasks) {
if (this.tasks[t].id === updatedTask.id) {
this.tasks[t] = updatedTask
break
}
}
// FIXME: Use computed
sortTasks(this.tasks)
},
async saveTaskPosition(e) {
this.drag = false
const task = this.tasks[e.newIndex]
const taskBefore = this.tasks[e.newIndex - 1] ?? null
const taskAfter = this.tasks[e.newIndex + 1] ?? null
const newTask = {
...task,
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
}
const updatedTask = await this.$store.dispatch('tasks/update', newTask)
this.tasks[e.newIndex] = updatedTask
},
}, },
}) })
const ctaVisible = ref(false)
const showTaskSearch = ref(false)
const drag = ref(false)
const DRAG_OPTIONS = {
animation: 100,
ghostClass: 'ghost',
} as const
const taskEditTask = ref(null)
const isTaskEdit = ref(false)
const {
tasks,
loading,
totalPages,
currentPage,
loadTasks,
searchTerm,
params,
// sortByParam,
} = useTaskList(toRef(props, 'listId'), {position: 'asc' })
const isAlphabeticalSorting = computed(() => {
return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
})
const firstNewPosition = computed(() => {
if (tasks.value.length === 0) {
return 0
}
return calculateItemPosition(null, tasks.value[0].position)
})
const store = useStore()
const list = computed(() => store.state.currentList)
const canWrite = computed(() => {
return list.value.maxRight > Rights.READ && list.value.id > 0
})
onMounted(async () => {
await nextTick()
ctaVisible.value = true
})
const route = useRoute()
const router = useRouter()
function searchTasks() {
// Only search if the search term changed
if (route.query as unknown as string === searchTerm.value) {
return
}
router.push({
name: 'list.list',
query: {search: searchTerm.value},
})
}
function hideSearchBar() {
// This is a workaround.
// When clicking on the search button, @blur from the input is fired. If we
// would then directly hide the whole search bar directly, no click event
// from the button gets fired. To prevent this, we wait 200ms until we hide
// everything so the button has a chance of firing the search event.
setTimeout(() => {
showTaskSearch.value = false
}, 200)
}
const addTaskRef = ref<typeof AddTask | null>(null)
function focusNewTaskInput() {
addTaskRef.value?.focusTaskInput()
}
function updateTaskList(task: ITask) {
if (isAlphabeticalSorting.value ) {
// reload tasks with current filter and sorting
loadTasks(1, undefined, undefined, true)
}
else {
tasks.value = [
task,
...tasks.value,
]
}
store.commit(HAS_TASKS, true)
}
function editTask(id: ITask['id']) {
taskEditTask.value = {...tasks.value.find(t => t.id === Number(id))}
isTaskEdit.value = true
}
function updateTasks(updatedTask: ITask) {
for (const t in tasks.value) {
if (tasks.value[t].id === updatedTask.id) {
tasks.value[t] = updatedTask
break
}
}
// FIXME: Use computed
sortTasks(tasks.value)
}
async function saveTaskPosition(e) {
drag.value = false
const task = tasks.value[e.newIndex]
const taskBefore = tasks.value[e.newIndex - 1] ?? null
const taskAfter = tasks.value[e.newIndex + 1] ?? null
const newTask = {
...task,
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
}
const updatedTask = await this.$store.dispatch('tasks/update', newTask)
this.tasks[e.newIndex] = updatedTask
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>