This is an implementation of the modals with the new possibilities of vue router 3. See: https://github.com/vuejs/vue-router/issues/703#issuecomment-865066913 for a better explanation and the linked example implementation: https://github.com/vuejs/vue-router-next/blob/master/e2e/modal/index.ts
372 lines
No EOL
8.7 KiB
Vue
372 lines
No EOL
8.7 KiB
Vue
<template>
|
|
<div
|
|
:class="{ 'is-loading': taskCollectionService.loading }"
|
|
class="loader-container is-max-width-desktop list-view"
|
|
>
|
|
<div
|
|
class="filter-container"
|
|
v-if="list.isSavedFilter && !list.isSavedFilter()"
|
|
>
|
|
<div class="items">
|
|
<div class="search">
|
|
<div :class="{ hidden: !showTaskSearch }" class="field has-addons">
|
|
<div class="control has-icons-left has-icons-right">
|
|
<input
|
|
@blur="hideSearchBar()"
|
|
@keyup.enter="searchTasks"
|
|
class="input"
|
|
:placeholder="$t('misc.search')"
|
|
type="text"
|
|
v-focus
|
|
v-model="searchTerm"
|
|
/>
|
|
<span class="icon is-left">
|
|
<icon icon="search"/>
|
|
</span>
|
|
</div>
|
|
<div class="control">
|
|
<x-button
|
|
:loading="taskCollectionService.loading"
|
|
@click="searchTasks"
|
|
:shadow="false"
|
|
>
|
|
{{ $t('misc.search') }}
|
|
</x-button>
|
|
</div>
|
|
</div>
|
|
<x-button
|
|
@click="showTaskSearch = !showTaskSearch"
|
|
icon="search"
|
|
variant="secondary"
|
|
v-if="!showTaskSearch"
|
|
/>
|
|
</div>
|
|
<filter-popup
|
|
v-model="params"
|
|
@update:modelValue="loadTasks()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<card :padding="false" :has-content="false" class="has-overflow">
|
|
<template
|
|
v-if="!list.isArchived && canWrite && list.id > 0"
|
|
>
|
|
<add-task
|
|
@taskAdded="updateTaskList"
|
|
ref="newTaskInput"
|
|
:default-position="firstNewPosition"
|
|
/>
|
|
</template>
|
|
|
|
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
|
|
{{ $t('list.list.empty') }}
|
|
<a @click="focusNewTaskInput()">
|
|
{{ $t('list.list.newTaskCta') }}
|
|
</a>
|
|
</nothing>
|
|
|
|
<div class="tasks-container" :class="{ 'has-task-edit-open': isTaskEdit }">
|
|
<div
|
|
class="tasks mt-0"
|
|
v-if="tasks && tasks.length > 0"
|
|
>
|
|
<draggable
|
|
v-bind="dragOptions"
|
|
v-model="tasks"
|
|
group="tasks"
|
|
@start="() => drag = true"
|
|
@end="saveTaskPosition"
|
|
handle=".handle"
|
|
:disabled="!canWrite"
|
|
item-key="id"
|
|
:component-data="{
|
|
class: { 'dragging-disabled': !canWrite || isAlphabeticalSorting },
|
|
}"
|
|
>
|
|
<template #item="{element: t}">
|
|
<single-task-in-list
|
|
:show-list-color="false"
|
|
:disabled="!canWrite"
|
|
:the-task="t"
|
|
@taskUpdated="updateTasks"
|
|
>
|
|
<template v-if="canWrite">
|
|
<span class="icon handle">
|
|
<icon icon="grip-lines"/>
|
|
</span>
|
|
<div
|
|
@click="editTask(t.id)"
|
|
class="icon settings"
|
|
v-if="!list.isArchived"
|
|
>
|
|
<icon icon="pencil-alt"/>
|
|
</div>
|
|
</template>
|
|
</single-task-in-list>
|
|
</template>
|
|
</draggable>
|
|
</div>
|
|
<edit-task
|
|
v-if="isTaskEdit"
|
|
class="taskedit mt-0"
|
|
:title="$t('list.list.editTask')"
|
|
@close="() => isTaskEdit = false"
|
|
:shadow="false"
|
|
:task="taskEditTask"
|
|
/>
|
|
</div>
|
|
|
|
<Pagination
|
|
:total-pages="taskCollectionService.totalPages"
|
|
:current-page="currentPage"
|
|
/>
|
|
</card>
|
|
|
|
<transition name="modal">
|
|
<task-detail-view-modal v-if="showTaskDetail" />
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import TaskService from '../../../services/task'
|
|
import TaskModel from '../../../models/task'
|
|
|
|
import EditTask from '../../../components/tasks/edit-task'
|
|
import AddTask from '../../../components/tasks/add-task'
|
|
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
|
|
import taskList from '../../../components/tasks/mixins/taskList'
|
|
import {saveListView} from '@/helpers/saveListView'
|
|
import Rights from '../../../models/constants/rights.json'
|
|
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
|
import {HAS_TASKS} from '@/store/mutation-types'
|
|
import Nothing from '@/components/misc/nothing.vue'
|
|
import Pagination from '@/components/misc/pagination.vue'
|
|
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
|
|
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
|
|
|
|
import draggable from 'vuedraggable'
|
|
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
|
|
|
function sortTasks(tasks) {
|
|
if (tasks === null || tasks === []) {
|
|
return
|
|
}
|
|
return tasks.sort((a, b) => {
|
|
if (a.done < b.done)
|
|
return -1
|
|
if (a.done > b.done)
|
|
return 1
|
|
|
|
if (a.position < b.position)
|
|
return -1
|
|
if (a.position > b.position)
|
|
return 1
|
|
return 0
|
|
})
|
|
}
|
|
|
|
export default {
|
|
name: 'List',
|
|
data() {
|
|
return {
|
|
taskService: new TaskService(),
|
|
isTaskEdit: false,
|
|
taskEditTask: TaskModel,
|
|
ctaVisible: false,
|
|
showTaskSearch: false,
|
|
|
|
drag: false,
|
|
dragOptions: {
|
|
animation: 100,
|
|
ghostClass: 'ghost',
|
|
},
|
|
}
|
|
},
|
|
mixins: [
|
|
taskList,
|
|
],
|
|
components: {
|
|
Nothing,
|
|
FilterPopup,
|
|
SingleTaskInList,
|
|
EditTask,
|
|
AddTask,
|
|
draggable,
|
|
Pagination,
|
|
TaskDetailViewModal,
|
|
},
|
|
|
|
setup() {
|
|
return {
|
|
showTaskDetail: useShowModal(),
|
|
}
|
|
},
|
|
|
|
created() {
|
|
// Save the current list view to local storage
|
|
// We use local storage and not vuex here to make it persistent across reloads.
|
|
saveListView(this.$route.params.listId, this.$route.name)
|
|
},
|
|
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 firering the search event.
|
|
setTimeout(() => {
|
|
this.showTaskSearch = false
|
|
}, 200)
|
|
},
|
|
// This function initializes the tasks page and loads the first page of tasks
|
|
initTasks(page, search = '') {
|
|
this.taskEditTask = null
|
|
this.isTaskEdit = false
|
|
this.loadTasks(page, search)
|
|
},
|
|
focusNewTaskInput() {
|
|
this.$refs.newTaskInput.$refs.newTaskInput.focus()
|
|
},
|
|
updateTaskList( task ) {
|
|
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) {
|
|
// Find the selected task and set it to the current object
|
|
let theTask = this.getTaskById(id) // Somehow this does not work if we directly assign this to this.taskEditTask
|
|
this.taskEditTask = theTask
|
|
this.isTaskEdit = true
|
|
},
|
|
getTaskById(id) {
|
|
for (const t in this.tasks) {
|
|
if (this.tasks[t].id === parseInt(id)) {
|
|
return this.tasks[t]
|
|
}
|
|
}
|
|
return {} // FIXME: This should probably throw something to make it clear to the user noting was found
|
|
},
|
|
updateTasks(updatedTask) {
|
|
for (const t in this.tasks) {
|
|
if (this.tasks[t].id === updatedTask.id) {
|
|
this.tasks[t] = updatedTask
|
|
break
|
|
}
|
|
}
|
|
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
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.tasks-container {
|
|
display: flex;
|
|
|
|
&.has-task-edit-open {
|
|
flex-direction: column;
|
|
|
|
@media screen and (min-width: $tablet) {
|
|
flex-direction: row;
|
|
|
|
.tasks {
|
|
width: 66%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.tasks {
|
|
width: 100%;
|
|
padding: .5rem;
|
|
|
|
.ghost {
|
|
border-radius: $radius;
|
|
background: var(--grey-100);
|
|
border: 2px dashed var(--grey-300);
|
|
|
|
* {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.taskedit {
|
|
width: 33%;
|
|
margin-right: 1rem;
|
|
margin-left: .5rem;
|
|
min-height: calc(100% - 1rem);
|
|
|
|
@media screen and (max-width: $tablet) {
|
|
width: 100%;
|
|
border-radius: 0;
|
|
margin: 0;
|
|
border-left: 0;
|
|
border-right: 0;
|
|
border-bottom: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.list-view .task-add {
|
|
padding: 1rem 1rem 0;
|
|
}
|
|
</style> |