forked from vikunja/frontend
konrad
6b1ebbabb7
Fix saving Cleanup Fix single value prepare Add prepare percent done stub Fix populating filters with saved values when editing for single values Fix populating filters with saved values when editing Add edit filter view page Hide adding new tasks to pseudolists Make sure all filter values are passed as strings as per requirement from the api Add redirect to list after creating it Add creating saved filter Add filter by percent done Add end date filter Add start date filter Add extra checkbox to enable/disable priority filter Add changing priority Add more filter stubs Fix dates for filters Add saved filter create form Add include nulls and concat to filter options Add new saved filter component Co-authored-by: kolaente <k@knt.li> Reviewed-on: vikunja/frontend#239 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
363 lines
10 KiB
Vue
363 lines
10 KiB
Vue
<template>
|
|
<div :class="{ 'is-loading': taskCollectionService.loading}" class="loader-container is-max-width-desktop">
|
|
<div class="filter-container">
|
|
<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="Search"
|
|
type="text"
|
|
v-focus
|
|
v-model="searchTerm"/>
|
|
<span class="icon is-left">
|
|
<icon icon="search"/>
|
|
</span>
|
|
</div>
|
|
<div class="control">
|
|
<button
|
|
:class="{'is-loading': taskCollectionService.loading}"
|
|
@click="searchTasks"
|
|
class="button noshadow is-primary">
|
|
Search
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button @click="showTaskSearch = !showTaskSearch" class="button" v-if="!showTaskSearch">
|
|
<span class="icon">
|
|
<icon icon="search"/>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<button @click="showTaskFilter = !showTaskFilter" class="button">
|
|
<span class="icon is-small">
|
|
<icon icon="filter"/>
|
|
</span>
|
|
Filters
|
|
</button>
|
|
</div>
|
|
<transition name="fade">
|
|
<filters
|
|
@change="loadTasks(1)"
|
|
v-if="showTaskFilter"
|
|
v-model="params"
|
|
/>
|
|
</transition>
|
|
</div>
|
|
|
|
<div class="field task-add" v-if="!list.isArchived && canWrite && list.id > 0">
|
|
<div class="field is-grouped">
|
|
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
|
|
<input
|
|
:class="{ 'disabled': taskService.loading}"
|
|
@keyup.enter="addTask()"
|
|
class="input"
|
|
placeholder="Add a new task..."
|
|
type="text"
|
|
v-focus
|
|
v-model="newTaskText"/>
|
|
<span class="icon is-small is-left">
|
|
<icon icon="tasks"/>
|
|
</span>
|
|
</p>
|
|
<p class="control">
|
|
<button :disabled="newTaskText.length === 0" @click="addTask()" class="button is-success">
|
|
<span class="icon is-small">
|
|
<icon icon="plus"/>
|
|
</span>
|
|
Add
|
|
</button>
|
|
</p>
|
|
</div>
|
|
<p class="help is-danger" v-if="showError && newTaskText === ''">
|
|
Please specify a list title.
|
|
</p>
|
|
</div>
|
|
|
|
<p class="list-is-empty-notice" v-if="tasks.length === 0">
|
|
This list is currently empty.
|
|
</p>
|
|
|
|
<div class="columns">
|
|
<div class="column">
|
|
<div :class="{'short': isTaskEdit}" class="tasks" v-if="tasks && tasks.length > 0">
|
|
<single-task-in-list
|
|
:disabled="!canWrite"
|
|
:key="t.id"
|
|
:the-task="t"
|
|
@taskUpdated="updateTasks"
|
|
task-detail-route="task.detail"
|
|
v-for="t in tasks"
|
|
>
|
|
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
|
|
<icon icon="pencil-alt"/>
|
|
</div>
|
|
</single-task-in-list>
|
|
</div>
|
|
</div>
|
|
<div class="column is-4" v-if="isTaskEdit">
|
|
<div class="card taskedit">
|
|
<header class="card-header">
|
|
<p class="card-header-title">
|
|
Edit Task
|
|
</p>
|
|
<a @click="isTaskEdit = false" class="card-header-icon">
|
|
<span class="icon">
|
|
<icon icon="angle-right"/>
|
|
</span>
|
|
</a>
|
|
</header>
|
|
<div class="card-content">
|
|
<div class="content">
|
|
<edit-task :task="taskEditTask"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav
|
|
aria-label="pagination"
|
|
class="pagination is-centered"
|
|
role="navigation"
|
|
v-if="taskCollectionService.totalPages > 1">
|
|
<router-link
|
|
:disabled="currentPage === 1"
|
|
:to="getRouteForPagination(currentPage - 1)"
|
|
class="pagination-previous"
|
|
tag="button">
|
|
Previous
|
|
</router-link>
|
|
<router-link
|
|
:disabled="currentPage === taskCollectionService.totalPages"
|
|
:to="getRouteForPagination(currentPage + 1)"
|
|
class="pagination-next"
|
|
tag="button">
|
|
Next page
|
|
</router-link>
|
|
<ul class="pagination-list">
|
|
<template v-for="(p, i) in pages">
|
|
<li :key="'page'+i" v-if="p.isEllipsis"><span class="pagination-ellipsis">…</span></li>
|
|
<li :key="'page'+i" v-else>
|
|
<router-link
|
|
:aria-label="'Goto page ' + p.number"
|
|
:class="{'is-current': p.number === currentPage}"
|
|
:to="getRouteForPagination(p.number)"
|
|
class="pagination-link">
|
|
{{ p.number }}
|
|
</router-link>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</nav>
|
|
|
|
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
|
<transition name="modal">
|
|
<router-view/>
|
|
</transition>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import TaskService from '../../../services/task'
|
|
import TaskModel from '../../../models/task'
|
|
import LabelTaskService from '../../../services/labelTask'
|
|
import LabelService from '../../../services/label'
|
|
import LabelTask from '../../../models/labelTask'
|
|
import LabelModel from '../../../models/label'
|
|
|
|
import EditTask from '../../../components/tasks/edit-task'
|
|
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
|
|
import taskList from '../../../components/tasks/mixins/taskList'
|
|
import {saveListView} from '@/helpers/saveListView'
|
|
import Filters from '../../../components/list/partials/filters'
|
|
import Rights from '../../../models/rights.json'
|
|
import {mapState} from 'vuex'
|
|
|
|
export default {
|
|
name: 'List',
|
|
data() {
|
|
return {
|
|
taskService: TaskService,
|
|
isTaskEdit: false,
|
|
taskEditTask: TaskModel,
|
|
newTaskText: '',
|
|
|
|
showError: false,
|
|
labelTaskService: LabelTaskService,
|
|
labelService: LabelService,
|
|
}
|
|
},
|
|
mixins: [
|
|
taskList,
|
|
],
|
|
components: {
|
|
Filters,
|
|
SingleTaskInList,
|
|
EditTask,
|
|
},
|
|
created() {
|
|
this.taskService = new TaskService()
|
|
this.labelService = new LabelService()
|
|
this.labelTaskService = new LabelTaskService()
|
|
|
|
// 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: mapState({
|
|
canWrite: state => state.currentList.maxRight > Rights.READ,
|
|
list: state => state.currentList,
|
|
}),
|
|
methods: {
|
|
// 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)
|
|
},
|
|
addTask() {
|
|
if (this.newTaskText === '') {
|
|
this.showError = true
|
|
return
|
|
}
|
|
this.showError = false
|
|
|
|
let task = new TaskModel({title: this.newTaskText, listId: this.$route.params.listId})
|
|
this.taskService.create(task)
|
|
.then(task => {
|
|
this.tasks.push(task)
|
|
this.sortTasks()
|
|
this.newTaskText = ''
|
|
|
|
// Check if the task has words starting with ~ in the title and make them to labels
|
|
const parts = task.title.split(' ~')
|
|
// The first element will always contain the title, even if there is no occurrence of ~
|
|
if (parts.length > 1) {
|
|
|
|
// First, create an unresolved promise for each entry in the array to wait
|
|
// until all labels are added to update the task title once again
|
|
let labelAddings = []
|
|
let labelAddsToWaitFor = []
|
|
parts.forEach((p, index) => {
|
|
if (index < 1) {
|
|
return
|
|
}
|
|
|
|
labelAddsToWaitFor.push(new Promise((resolve, reject) => {
|
|
labelAddings.push({resolve: resolve, reject: reject})
|
|
}))
|
|
})
|
|
|
|
// Then do everything that is involved in finding, creating and adding the label to the task
|
|
parts.forEach((p, index) => {
|
|
if (index < 1) {
|
|
return
|
|
}
|
|
|
|
// The part up until the next space
|
|
const labelTitle = p.split(' ')[0]
|
|
|
|
// Check if the label exists
|
|
this.labelService.getAll({}, {s: labelTitle})
|
|
.then(res => {
|
|
// Label found, use it
|
|
if (res.length > 0 && res[0].title === labelTitle) {
|
|
const labelTask = new LabelTask({
|
|
taskId: task.id,
|
|
labelId: res[0].id,
|
|
})
|
|
this.labelTaskService.create(labelTask)
|
|
.then(result => {
|
|
task.labels.push(res[0])
|
|
|
|
// Remove the label text from the task title
|
|
task.title = task.title.replace(` ~${labelTitle}`, '')
|
|
|
|
// Make the promise done (the one with the index 0 does not exist)
|
|
labelAddings[index - 1].resolve(result)
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
} else {
|
|
// label not found, create it
|
|
const label = new LabelModel({title: labelTitle})
|
|
this.labelService.create(label)
|
|
.then(res => {
|
|
const labelTask = new LabelTask({
|
|
taskId: task.id,
|
|
labelId: res.id,
|
|
})
|
|
this.labelTaskService.create(labelTask)
|
|
.then(result => {
|
|
task.labels.push(res)
|
|
|
|
// Remove the label text from the task title
|
|
task.title = task.title.replace(` ~${labelTitle}`, '')
|
|
|
|
// Make the promise done (the one with the index 0 does not exist)
|
|
labelAddings[index - 1].resolve(result)
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
}
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
})
|
|
|
|
// This waits to update the task until all labels have been added and the title has
|
|
// been modified to remove each label text
|
|
Promise.all(labelAddsToWaitFor)
|
|
.then(() => {
|
|
this.taskService.update(task)
|
|
.then(updatedTask => {
|
|
this.updateTasks(updatedTask)
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
},
|
|
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.$set(this.tasks, t, updatedTask)
|
|
break
|
|
}
|
|
}
|
|
this.sortTasks()
|
|
},
|
|
},
|
|
}
|
|
</script> |