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:
parent
63f2e6ba6f
commit
bbf4ef4697
6 changed files with 163 additions and 177 deletions
|
@ -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>
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -8,3 +8,7 @@ export function getSavedFilterIdFromListId(listId: IList['id']) {
|
||||||
}
|
}
|
||||||
return filterId
|
return filterId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSavedFilter(list: IList) {
|
||||||
|
return getSavedFilterIdFromListId(list.id) > 0
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,142 +180,129 @@ function sortTasks(tasks: ITask[]) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps({
|
||||||
name: 'List',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
listId: {
|
listId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
|
|
||||||
data() {
|
const ctaVisible = ref(false)
|
||||||
return {
|
const showTaskSearch = ref(false)
|
||||||
ctaVisible: false,
|
|
||||||
showTaskSearch: false,
|
|
||||||
|
|
||||||
drag: false,
|
const drag = ref(false)
|
||||||
dragOptions: {
|
const DRAG_OPTIONS = {
|
||||||
animation: 100,
|
animation: 100,
|
||||||
ghostClass: 'ghost',
|
ghostClass: 'ghost',
|
||||||
},
|
} as const
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
BaseButton,
|
|
||||||
ListWrapper,
|
|
||||||
Nothing,
|
|
||||||
FilterPopup,
|
|
||||||
SingleTaskInList,
|
|
||||||
EditTask,
|
|
||||||
AddTask,
|
|
||||||
draggable,
|
|
||||||
Pagination,
|
|
||||||
ButtonLink,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const taskEditTask = ref(null)
|
const taskEditTask = ref(null)
|
||||||
const isTaskEdit = ref(false)
|
const isTaskEdit = ref(false)
|
||||||
|
|
||||||
// This function initializes the tasks page and loads the first page of tasks
|
const {
|
||||||
// function beforeLoad() {
|
tasks,
|
||||||
// taskEditTask.value = null
|
loading,
|
||||||
// isTaskEdit.value = false
|
totalPages,
|
||||||
// }
|
currentPage,
|
||||||
|
loadTasks,
|
||||||
|
searchTerm,
|
||||||
|
params,
|
||||||
|
// sortByParam,
|
||||||
|
} = useTaskList(toRef(props, 'listId'), {position: 'asc' })
|
||||||
|
|
||||||
const taskList = useTaskList(toRef(props, 'listId'), {
|
|
||||||
position: 'asc',
|
const isAlphabeticalSorting = computed(() => {
|
||||||
|
return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
const firstNewPosition = computed(() => {
|
||||||
taskEditTask,
|
if (tasks.value.length === 0) {
|
||||||
isTaskEdit,
|
|
||||||
...taskList,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isAlphabeticalSorting() {
|
|
||||||
return this.params.sort_by.find( sortBy => sortBy === ALPHABETICAL_SORT ) !== undefined
|
|
||||||
},
|
|
||||||
firstNewPosition() {
|
|
||||||
if (this.tasks.length === 0) {
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculateItemPosition(null, this.tasks[0].position)
|
return calculateItemPosition(null, tasks.value[0].position)
|
||||||
},
|
})
|
||||||
canWrite() {
|
|
||||||
return this.list.maxRight > Rights.READ && this.list.id > 0
|
const store = useStore()
|
||||||
},
|
const list = computed(() => store.state.currentList)
|
||||||
list() {
|
|
||||||
return this.$store.state.currentList
|
const canWrite = computed(() => {
|
||||||
},
|
return list.value.maxRight > Rights.READ && list.value.id > 0
|
||||||
},
|
})
|
||||||
mounted() {
|
|
||||||
this.$nextTick(() => (this.ctaVisible = true))
|
onMounted(async () => {
|
||||||
},
|
await nextTick()
|
||||||
methods: {
|
ctaVisible.value = true
|
||||||
searchTasks() {
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
function searchTasks() {
|
||||||
// Only search if the search term changed
|
// Only search if the search term changed
|
||||||
if (this.$route.query === this.searchTerm) {
|
if (route.query as unknown as string === searchTerm.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$router.push({
|
router.push({
|
||||||
name: 'list.list',
|
name: 'list.list',
|
||||||
query: {search: this.searchTerm},
|
query: {search: searchTerm.value},
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
hideSearchBar() {
|
|
||||||
|
function hideSearchBar() {
|
||||||
// This is a workaround.
|
// This is a workaround.
|
||||||
// When clicking on the search button, @blur from the input is fired. If we
|
// 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
|
// 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
|
// 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.
|
// everything so the button has a chance of firing the search event.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showTaskSearch = false
|
showTaskSearch.value = false
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
}
|
||||||
focusNewTaskInput() {
|
|
||||||
this.$refs.addTask.focusTaskInput()
|
const addTaskRef = ref<typeof AddTask | null>(null)
|
||||||
},
|
function focusNewTaskInput() {
|
||||||
updateTaskList(task: ITask) {
|
addTaskRef.value?.focusTaskInput()
|
||||||
if ( this.isAlphabeticalSorting ) {
|
}
|
||||||
|
|
||||||
|
function updateTaskList(task: ITask) {
|
||||||
|
if (isAlphabeticalSorting.value ) {
|
||||||
// reload tasks with current filter and sorting
|
// reload tasks with current filter and sorting
|
||||||
this.loadTasks(1, undefined, undefined, true)
|
loadTasks(1, undefined, undefined, true)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.tasks = [
|
tasks.value = [
|
||||||
task,
|
task,
|
||||||
...this.tasks,
|
...tasks.value,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit(HAS_TASKS, true)
|
store.commit(HAS_TASKS, true)
|
||||||
},
|
}
|
||||||
editTask(id: ITask['id']) {
|
|
||||||
this.taskEditTask = {...this.tasks.find(t => t.id === parseInt(id))}
|
function editTask(id: ITask['id']) {
|
||||||
this.isTaskEdit = true
|
taskEditTask.value = {...tasks.value.find(t => t.id === Number(id))}
|
||||||
},
|
isTaskEdit.value = true
|
||||||
updateTasks(updatedTask: ITask) {
|
}
|
||||||
for (const t in this.tasks) {
|
|
||||||
if (this.tasks[t].id === updatedTask.id) {
|
function updateTasks(updatedTask: ITask) {
|
||||||
this.tasks[t] = updatedTask
|
for (const t in tasks.value) {
|
||||||
|
if (tasks.value[t].id === updatedTask.id) {
|
||||||
|
tasks.value[t] = updatedTask
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FIXME: Use computed
|
// FIXME: Use computed
|
||||||
sortTasks(this.tasks)
|
sortTasks(tasks.value)
|
||||||
},
|
}
|
||||||
|
|
||||||
async saveTaskPosition(e) {
|
async function saveTaskPosition(e) {
|
||||||
this.drag = false
|
drag.value = false
|
||||||
|
|
||||||
const task = this.tasks[e.newIndex]
|
const task = tasks.value[e.newIndex]
|
||||||
const taskBefore = this.tasks[e.newIndex - 1] ?? null
|
const taskBefore = tasks.value[e.newIndex - 1] ?? null
|
||||||
const taskAfter = this.tasks[e.newIndex + 1] ?? null
|
const taskAfter = tasks.value[e.newIndex + 1] ?? null
|
||||||
|
|
||||||
const newTask = {
|
const newTask = {
|
||||||
...task,
|
...task,
|
||||||
|
@ -317,9 +311,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const updatedTask = await this.$store.dispatch('tasks/update', newTask)
|
const updatedTask = await this.$store.dispatch('tasks/update', newTask)
|
||||||
this.tasks[e.newIndex] = updatedTask
|
this.tasks[e.newIndex] = updatedTask
|
||||||
},
|
}
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
Loading…
Reference in a new issue