Table View for tasks #76
23
src/App.vue
23
src/App.vue
|
@ -104,18 +104,9 @@
|
|||
</ul>
|
||||
</div>
|
||||
<aside class="menu namespaces-lists">
|
||||
<div class="fancycheckbox show-archived-check">
|
||||
<input type="checkbox" v-model="showArchived" @change="loadNamespaces()" style="display: none;" id="showArchivedCheckbox"/>
|
||||
<label class="check" for="showArchivedCheckbox">
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18">
|
||||
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
||||
<polyline points="1 9 7 14 15 4"></polyline>
|
||||
</svg>
|
||||
<span>
|
||||
Show Archived
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<fancycheckbox v-model="showArchived" @change="loadNamespaces()" class="show-archived-check">
|
||||
Show Archived
|
||||
</fancycheckbox>
|
||||
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id">
|
||||
|
@ -228,11 +219,15 @@
|
|||
import authTypes from './models/authTypes'
|
||||
|
||||
import swEvents from './ServiceWorker/events'
|
||||
import Notification from "./components/global/notification";
|
||||
import Notification from './components/global/notification'
|
||||
import Fancycheckbox from './components/global/fancycheckbox'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {Notification},
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
Notification,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: auth.user,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div class="fancycheckbox" :class="{'is-disabled': disabled}">
|
||||
<input @change="updateData" type="checkbox" :id="checkBoxId" :checked="checked" style="display: none;" :disabled="disabled">
|
||||
<label :for="checkBoxId" class="check">
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18">
|
||||
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
||||
<polyline points="1 9 7 14 15 4"></polyline>
|
||||
</svg>
|
||||
<span>
|
||||
<slot></slot>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'fancycheckbox',
|
||||
data() {
|
||||
return {
|
||||
checked: false,
|
||||
checkBoxId: '',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.checked = newVal
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.checked = this.value
|
||||
},
|
||||
created() {
|
||||
this.checkBoxId = 'fancycheckbox' + Math.random()
|
||||
},
|
||||
methods: {
|
||||
updateData(e) {
|
||||
this.checked = e.target.checked
|
||||
this.$emit('input', this.checked)
|
||||
this.$emit('change', e.target.checked)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="user">
|
||||
<div class="user" :class="{'is-inline': isInline}">
|
||||
<img :src="user.getAvatarUrl(avatarSize)" class="avatar" alt="" v-tooltip="user.username" :width="avatarSize" :height="avatarSize"/>
|
||||
<span v-if="showUsername" class="username">{{ user.username }}</span>
|
||||
</div>
|
||||
|
@ -22,7 +22,11 @@
|
|||
required: false,
|
||||
type: Number,
|
||||
default: 50,
|
||||
}
|
||||
},
|
||||
isInline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -31,6 +35,10 @@
|
|||
.user {
|
||||
margin: .5em;
|
||||
|
||||
&.is-inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-border-radius: 100%;
|
||||
-moz-border-radius: 100%;
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
{{ l.title }}
|
||||
</a>
|
||||
<a class="delete is-small" @click="deleteLabel(l)" v-if="user.infos.id === l.created_by.id"></a>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-4" v-if="isLabelEdit">
|
||||
|
|
|
@ -28,10 +28,9 @@
|
|||
<div class="field">
|
||||
<label class="label" for="isArchivedCheck">Is Archived</label>
|
||||
<div class="control">
|
||||
<label class="checkbox" v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
|
||||
<input type="checkbox" id="isArchivedCheck" v-model="list.is_archived"/>
|
||||
<fancycheckbox v-model="list.is_archived" v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
|
||||
This list is archived
|
||||
</label>
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -89,10 +88,11 @@
|
|||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import manageSharing from '../sharing/userTeam'
|
||||
import LinkSharing from '../sharing/linkSharing';
|
||||
import LinkSharing from '../sharing/linkSharing'
|
||||
|
||||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import Fancycheckbox from '../global/fancycheckbox'
|
||||
|
||||
export default {
|
||||
name: "EditList",
|
||||
|
@ -110,6 +110,7 @@
|
|||
}
|
||||
},
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
LinkSharing,
|
||||
manageSharing,
|
||||
verte,
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
It is not possible to create new or edit tasks or it.
|
||||
</div>
|
||||
<div class="switch-view">
|
||||
<router-link :to="{ name: 'showList', params: { id: list.id } }" :class="{'is-active': $route.params.type !== 'gantt'}">List</router-link>
|
||||
<router-link :to="{ name: 'showList', params: { id: list.id } }" :class="{'is-active': $route.params.type !== 'gantt' && $route.params.type !== 'table'}">List</router-link>
|
||||
<router-link :to="{ name: 'showListWithType', params: { id: list.id, type: 'gantt' } }" :class="{'is-active': $route.params.type === 'gantt'}">Gantt</router-link>
|
||||
<router-link :to="{ name: 'showListWithType', params: { id: list.id, type: 'table' } }" :class="{'is-active': $route.params.type === 'table'}">Table</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gantt :list="list" v-if="$route.params.type === 'gantt'"/>
|
||||
<table-view :list="list" v-else-if="$route.params.type === 'table'"/>
|
||||
<show-list-task :the-list="list" v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -30,6 +32,7 @@
|
|||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import authType from '../../models/authTypes'
|
||||
import TableView from '../tasks/TableView'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -40,6 +43,7 @@
|
|||
}
|
||||
},
|
||||
components: {
|
||||
TableView,
|
||||
Gantt,
|
||||
ShowListTask,
|
||||
},
|
||||
|
@ -50,7 +54,13 @@
|
|||
}
|
||||
|
||||
// If the type is invalid, redirect the user
|
||||
if (auth.user.authenticated && auth.user.infos.type !== authType.LINK_SHARE && this.$route.params.type !== 'gantt' && this.$route.params.type !== '') {
|
||||
if (
|
||||
auth.user.authenticated &&
|
||||
auth.user.infos.type !== authType.LINK_SHARE &&
|
||||
this.$route.params.type !== 'gantt' &&
|
||||
this.$route.params.type !== 'table' &&
|
||||
this.$route.params.type !== ''
|
||||
) {
|
||||
router.push({name: 'showList', params: { id: this.$route.params.id }})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -28,18 +28,9 @@
|
|||
<div class="field">
|
||||
<label class="label" for="isArchivedCheck">Is Archived</label>
|
||||
<div class="control">
|
||||
<div class="fancycheckbox" v-tooltip="'If a namespace is archived, you cannot create new lists or edit it.'">
|
||||
<input type="checkbox" id="isArchivedCheck" v-model="namespace.is_archived"/>
|
||||
<label class="check" for="isArchivedCheck">
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18">
|
||||
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
||||
<polyline points="1 9 7 14 15 4"></polyline>
|
||||
</svg>
|
||||
<span>
|
||||
This namespace is archived
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<fancycheckbox v-model="namespace.is_archived" v-tooltip="'If a namespace is archived, you cannot create new lists or edit it.'">
|
||||
This namespace is archived
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -98,6 +89,7 @@
|
|||
|
||||
import NamespaceService from '../../services/namespace'
|
||||
import NamespaceModel from '../../models/namespace'
|
||||
import Fancycheckbox from '../global/fancycheckbox'
|
||||
|
||||
export default {
|
||||
name: "EditNamespace",
|
||||
|
@ -114,6 +106,7 @@
|
|||
}
|
||||
},
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
manageSharing,
|
||||
verte,
|
||||
},
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="gantt-options">
|
||||
<div class="fancycheckbox is-block">
|
||||
<input id="showTaskswithoutDates" type="checkbox" style="display: none;" v-model="showTaskswithoutDates">
|
||||
<label for="showTaskswithoutDates" class="check">
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18">
|
||||
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
||||
<polyline points="1 9 7 14 15 4"></polyline>
|
||||
</svg>
|
||||
<span>
|
||||
Show tasks which don't have dates set
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<fancycheckbox v-model="showTaskswithoutDates" class="is-block">
|
||||
Show tasks which don't have dates set
|
||||
</fancycheckbox>
|
||||
<div class="range-picker">
|
||||
<div class="field">
|
||||
<label class="label" for="dayWidth">Size</label>
|
||||
|
@ -66,10 +57,12 @@
|
|||
import GanttChart from './gantt-component'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import ListModel from '../../models/list'
|
||||
import Fancycheckbox from '../global/fancycheckbox'
|
||||
|
||||
export default {
|
||||
name: 'Gantt',
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
flatPickr,
|
||||
GanttChart
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="loader-container" :class="{ 'is-loading': listService.loading || taskCollectionService.loading}">
|
||||
<div class="loader-container" :class="{ 'is-loading': taskCollectionService.loading}">
|
||||
<div class="search">
|
||||
<div class="field has-addons" :class="{ 'hidden': !showTaskSearch }">
|
||||
<div class="control has-icons-left has-icons-right">
|
||||
|
@ -87,13 +87,13 @@
|
|||
</div>
|
||||
|
||||
<nav class="pagination is-centered" role="navigation" aria-label="pagination" v-if="taskCollectionService.totalPages > 1">
|
||||
<router-link class="pagination-previous" :to="{name: 'showList', query: { page: currentPage - 1 }}" tag="button" :disabled="currentPage === 1">Previous</router-link>
|
||||
<router-link class="pagination-next" :to="{name: 'showList', query: { page: currentPage + 1 }}" tag="button" :disabled="currentPage === taskCollectionService.totalPages">Next page</router-link>
|
||||
<router-link class="pagination-previous" :to="getRouteForPagination(currentPage - 1)" tag="button" :disabled="currentPage === 1">Previous</router-link>
|
||||
<router-link class="pagination-next" :to="getRouteForPagination(currentPage + 1)" tag="button" :disabled="currentPage === taskCollectionService.totalPages">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 :to="{name: 'showList', query: { page: p.number }}" :class="{'is-current': p.number === currentPage}" class="pagination-link" :aria-label="'Goto page ' + p.number">{{ p.number }}</router-link>
|
||||
<router-link :to="getRouteForPagination(p.number)" :class="{'is-current': p.number === currentPage}" class="pagination-link" :aria-label="'Goto page ' + p.number">{{ p.number }}</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
@ -102,35 +102,30 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import ListModel from '../../models/list'
|
||||
import EditTask from './edit-task'
|
||||
import TaskModel from '../../models/task'
|
||||
import TaskCollectionService from '../../services/taskCollection'
|
||||
import SingleTaskInList from './reusable/singleTaskInList'
|
||||
import taskList from './helpers/taskList'
|
||||
|
||||
export default {
|
||||
name: 'ListView',
|
||||
data() {
|
||||
return {
|
||||
listID: this.$route.params.id,
|
||||
listService: ListService,
|
||||
taskService: TaskService,
|
||||
taskCollectionService: TaskCollectionService,
|
||||
pages: [],
|
||||
currentPage: 0,
|
||||
list: {},
|
||||
tasks: [],
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
newTaskText: '',
|
||||
|
||||
showError: false,
|
||||
|
||||
showTaskSearch: false,
|
||||
searchTerm: '',
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
taskList,
|
||||
],
|
||||
components: {
|
||||
SingleTaskInList,
|
||||
EditTask,
|
||||
|
@ -145,12 +140,9 @@
|
|||
theList() {
|
||||
this.list = this.theList
|
||||
},
|
||||
'$route.query': 'loadTasksForPage', // Only listen for query path changes
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.taskService = new TaskService()
|
||||
this.taskCollectionService = new TaskCollectionService()
|
||||
this.initTasks(1)
|
||||
},
|
||||
methods: {
|
||||
|
@ -179,61 +171,6 @@
|
|||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
loadTasks(page, search = '') {
|
||||
const params = {sort_by: ['done', 'id'], order_by: ['asc', 'desc']}
|
||||
if (search !== '') {
|
||||
params.s = search
|
||||
}
|
||||
this.taskCollectionService.getAll({listID: this.$route.params.id}, params, page)
|
||||
.then(r => {
|
||||
this.$set(this, 'tasks', r)
|
||||
this.$set(this, 'pages', [])
|
||||
this.currentPage = page
|
||||
|
||||
for (let i = 0; i < this.taskCollectionService.totalPages; i++) {
|
||||
|
||||
// Show ellipsis instead of all pages
|
||||
if(
|
||||
i > 0 && // Always at least the first page
|
||||
(i + 1) < this.taskCollectionService.totalPages && // And the last page
|
||||
(
|
||||
// And the current with current + 1 and current - 1
|
||||
(i + 1) > this.currentPage + 1 ||
|
||||
(i + 1) < this.currentPage - 1
|
||||
)
|
||||
) {
|
||||
// Only add an ellipsis if the last page isn't already one
|
||||
if(this.pages[i - 1] && !this.pages[i - 1].isEllipsis) {
|
||||
this.pages.push({
|
||||
number: 0,
|
||||
isEllipsis: true,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.pages.push({
|
||||
number: i + 1,
|
||||
isEllipsis: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
loadTasksForPage(e) {
|
||||
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
|
||||
let page = e.page
|
||||
if (typeof e.page === 'undefined') {
|
||||
page = 1
|
||||
}
|
||||
let search = e.search
|
||||
if (typeof e.search === 'undefined') {
|
||||
search = ''
|
||||
}
|
||||
this.initTasks(page, search)
|
||||
},
|
||||
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
|
||||
|
@ -248,23 +185,6 @@
|
|||
}
|
||||
return {} // FIXME: This should probably throw something to make it clear to the user noting was found
|
||||
},
|
||||
sortTasks() {
|
||||
if (this.tasks === null || this.tasks === []) {
|
||||
return
|
||||
}
|
||||
return this.tasks.sort(function(a,b) {
|
||||
if (a.done < b.done)
|
||||
return -1
|
||||
if (a.done > b.done)
|
||||
return 1
|
||||
|
||||
if (a.id > b.id)
|
||||
return -1
|
||||
if (a.id < b.id)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
updateTasks(updatedTask) {
|
||||
for (const t in this.tasks) {
|
||||
if (this.tasks[t].id === updatedTask.id) {
|
||||
|
@ -274,25 +194,6 @@
|
|||
}
|
||||
this.sortTasks()
|
||||
},
|
||||
searchTasks() {
|
||||
if (this.searchTerm === '') {
|
||||
return
|
||||
}
|
||||
this.$router.push({
|
||||
name: 'showList',
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,234 @@
|
|||
<template>
|
||||
<div class="table-view loader-container" :class="{'is-loading': taskCollectionService.loading}">
|
||||
<div class="column-filter">
|
||||
<button class="button" @click="showActiveColumnsFilter = !showActiveColumnsFilter">
|
||||
<span class="icon is-small">
|
||||
<icon icon="th"/>
|
||||
</span>
|
||||
Columns
|
||||
</button>
|
||||
<transition name="fade">
|
||||
<div class="card" v-if="showActiveColumnsFilter">
|
||||
<div class="card-content">
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">Done</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.text">Name</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">Priority</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">Labels</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">Assignees</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">Due Date</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">Start Date</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">End Date</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">% Done</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">Created</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">Updated</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">Created By</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<table class="table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="activeColumns.id">
|
||||
#
|
||||
<sort :order="sortBy.id" @click="sort('id')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.done">
|
||||
Done
|
||||
<sort :order="sortBy.done" @click="sort('done')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.text">
|
||||
Name
|
||||
<sort :order="sortBy.text" @click="sort('text')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.priority">
|
||||
Priority
|
||||
<sort :order="sortBy.priority" @click="sort('priority')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.labels">
|
||||
Labels
|
||||
</th>
|
||||
<th v-if="activeColumns.assignees">
|
||||
Assignees
|
||||
</th>
|
||||
<th v-if="activeColumns.dueDate">
|
||||
Due Date
|
||||
<sort :order="sortBy.due_date_unix" @click="sort('due_date_unix')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.startDate">
|
||||
Start Date
|
||||
<sort :order="sortBy.start_date_unix" @click="sort('start_date_unix')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.endDate">
|
||||
End Date
|
||||
<sort :order="sortBy.end_date_unix" @click="sort('end_date_unix')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.percentDone">
|
||||
% Done
|
||||
<sort :order="sortBy.percent_done" @click="sort('percent_done')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.created">
|
||||
Created
|
||||
<sort :order="sortBy.created" @click="sort('created')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.updated">
|
||||
Updated
|
||||
<sort :order="sortBy.updated" @click="sort('updated')"/>
|
||||
</th>
|
||||
<th v-if="activeColumns.createdBy">
|
||||
Created By
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in tasks" :key="t.id">
|
||||
<td v-if="activeColumns.id">
|
||||
<router-link :to="{name: 'taskDetailView', params: { id: t.id }}">{{ t.id }}</router-link>
|
||||
</td>
|
||||
<td v-if="activeColumns.done">
|
||||
<div class="is-done" v-if="t.done">Done</div>
|
||||
</td>
|
||||
<td v-if="activeColumns.text">
|
||||
<router-link :to="{name: 'taskDetailView', params: { id: t.id }}">{{ t.text }}</router-link>
|
||||
</td>
|
||||
<td v-if="activeColumns.priority">
|
||||
<priority-label :priority="t.priority" :show-all="true"/>
|
||||
</td>
|
||||
<td v-if="activeColumns.labels">
|
||||
<labels :labels="t.labels"/>
|
||||
</td>
|
||||
<td v-if="activeColumns.assignees">
|
||||
<user
|
||||
:user="a"
|
||||
:avatar-size="27"
|
||||
:show-username="false"
|
||||
:is-inline="true"
|
||||
v-for="(a, i) in t.assignees"
|
||||
:key="t.id + 'assignee' + a.id + i"
|
||||
/>
|
||||
</td>
|
||||
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
|
||||
<date-table-cell :date="t.startDate" v-if="activeColumns.startDate"/>
|
||||
<date-table-cell :date="t.endDate" v-if="activeColumns.endDate"/>
|
||||
<td v-if="activeColumns.percentDone">{{ t.percentDone }}%</td>
|
||||
<date-table-cell :date="t.created" v-if="activeColumns.created"/>
|
||||
<date-table-cell :date="t.updated" v-if="activeColumns.updated"/>
|
||||
<td v-if="activeColumns.createdBy">
|
||||
<user
|
||||
:user="t.createdBy"
|
||||
:show-username="false"
|
||||
:avatar-size="27"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nav class="pagination is-centered" role="navigation" aria-label="pagination" v-if="taskCollectionService.totalPages > 1">
|
||||
<router-link class="pagination-previous" :to="getRouteForPagination(currentPage - 1, 'table')" tag="button" :disabled="currentPage === 1">Previous</router-link>
|
||||
<router-link class="pagination-next" :to="getRouteForPagination(currentPage + 1, 'table')" tag="button" :disabled="currentPage === taskCollectionService.totalPages">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 :to="getRouteForPagination(p.number, 'table')" :class="{'is-current': p.number === currentPage}" class="pagination-link" :aria-label="'Goto page ' + p.number">{{ p.number }}</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListModel from '../../models/list'
|
||||
import taskList from './helpers/taskList'
|
||||
import User from '../global/user'
|
||||
import PriorityLabel from './reusable/priorityLabel'
|
||||
import Labels from './reusable/labels'
|
||||
import DateTableCell from './reusable/date-table-cell'
|
||||
import Fancycheckbox from '../global/fancycheckbox'
|
||||
import Sort from './reusable/sort'
|
||||
|
||||
export default {
|
||||
name: 'TableView',
|
||||
components: {
|
||||
Sort,
|
||||
Fancycheckbox,
|
||||
DateTableCell,
|
||||
Labels,
|
||||
PriorityLabel,
|
||||
User,
|
||||
},
|
||||
mixins: [
|
||||
taskList,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
showActiveColumnsFilter: false,
|
||||
activeColumns: {
|
||||
id: true,
|
||||
done: true,
|
||||
text: true,
|
||||
priority: false,
|
||||
labels: true,
|
||||
assignees: true,
|
||||
dueDate: true,
|
||||
startDate: false,
|
||||
endDate: false,
|
||||
percentDone: false,
|
||||
created: false,
|
||||
updated: false,
|
||||
createdBy: false,
|
||||
},
|
||||
sortBy: {
|
||||
id: 'desc',
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: ListModel,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const savedShowColumns = localStorage.getItem('tableViewColumns')
|
||||
if (savedShowColumns !== null) {
|
||||
this.$set(this, 'activeColumns', JSON.parse(savedShowColumns))
|
||||
}
|
||||
const savedSortBy = localStorage.getItem('tableViewSortBy')
|
||||
if (savedSortBy !== null) {
|
||||
this.$set(this, 'sortBy', JSON.parse(savedSortBy))
|
||||
}
|
||||
|
||||
this.initTasks(1)
|
||||
},
|
||||
methods: {
|
||||
initTasks(page, search = '') {
|
||||
let params = {sort_by: [], order_by: []}
|
||||
Object.keys(this.sortBy).map(s => {
|
||||
params.sort_by.push(s)
|
||||
params.order_by.push(this.sortBy[s])
|
||||
})
|
||||
this.loadTasks(page, search, params)
|
||||
},
|
||||
sort(property) {
|
||||
const order = this.sortBy[property]
|
||||
if (typeof order === 'undefined' || order === 'none') {
|
||||
this.$set(this.sortBy, property, 'desc')
|
||||
} else if (order === 'desc') {
|
||||
this.$set(this.sortBy, property, 'asc')
|
||||
} else {
|
||||
this.$delete(this.sortBy, property)
|
||||
}
|
||||
this.initTasks(this.currentPage, this.searchTerm)
|
||||
// Save the order to be able to retrieve them later
|
||||
localStorage.setItem('tableViewSortBy', JSON.stringify(this.sortBy))
|
||||
},
|
||||
saveTaskColumns() {
|
||||
localStorage.setItem('tableViewColumns', JSON.stringify(this.activeColumns))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,128 @@
|
|||
import TaskCollectionService from '../../../services/taskCollection'
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
taskCollectionService: TaskCollectionService,
|
||||
tasks: [],
|
||||
|
||||
pages: [],
|
||||
currentPage: 0,
|
||||
|
||||
showTaskSearch: false,
|
||||
searchTerm: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query': 'loadTasksForPage', // Only listen for query path changes
|
||||
},
|
||||
created() {
|
||||
this.taskCollectionService = new TaskCollectionService()
|
||||
},
|
||||
methods: {
|
||||
loadTasks(page, search = '', params = {sort_by: ['done', 'id'], order_by: ['asc', 'desc']}) {
|
||||
if (search !== '') {
|
||||
params.s = search
|
||||
}
|
||||
this.taskCollectionService.getAll({listID: this.$route.params.id}, params, page)
|
||||
.then(r => {
|
||||
this.$set(this, 'tasks', r)
|
||||
this.$set(this, 'pages', [])
|
||||
this.currentPage = page
|
||||
|
||||
for (let i = 0; i < this.taskCollectionService.totalPages; i++) {
|
||||
|
||||
// Show ellipsis instead of all pages
|
||||
if(
|
||||
i > 0 && // Always at least the first page
|
||||
(i + 1) < this.taskCollectionService.totalPages && // And the last page
|
||||
(
|
||||
// And the current with current + 1 and current - 1
|
||||
(i + 1) > this.currentPage + 1 ||
|
||||
(i + 1) < this.currentPage - 1
|
||||
)
|
||||
) {
|
||||
// Only add an ellipsis if the last page isn't already one
|
||||
if(this.pages[i - 1] && !this.pages[i - 1].isEllipsis) {
|
||||
this.pages.push({
|
||||
number: 0,
|
||||
isEllipsis: true,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.pages.push({
|
||||
number: i + 1,
|
||||
isEllipsis: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
loadTasksForPage(e) {
|
||||
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
|
||||
let page = e.page
|
||||
if (typeof e.page === 'undefined') {
|
||||
page = 1
|
||||
}
|
||||
let search = e.search
|
||||
if (typeof e.search === 'undefined') {
|
||||
search = ''
|
||||
}
|
||||
this.initTasks(page, search)
|
||||
},
|
||||
sortTasks() {
|
||||
if (this.tasks === null || this.tasks === []) {
|
||||
return
|
||||
}
|
||||
return this.tasks.sort(function(a,b) {
|
||||
if (a.done < b.done)
|
||||
return -1
|
||||
if (a.done > b.done)
|
||||
return 1
|
||||
|
||||
if (a.id > b.id)
|
||||
return -1
|
||||
if (a.id < b.id)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
searchTasks() {
|
||||
if (this.searchTerm === '') {
|
||||
return
|
||||
}
|
||||
this.$router.push({
|
||||
name: 'showList',
|
||||
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)
|
||||
},
|
||||
getRouteForPagination(page = 1, type = 'list') {
|
||||
return {
|
||||
name: 'showListWithType',
|
||||
params: {
|
||||
type: type
|
||||
},
|
||||
query: {
|
||||
page: page,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<td v-tooltip="+date === 0 ? '' : formatDate(date)">
|
||||
{{ +date === 0 ? '-' : formatDateSince(date) }}
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'date-table-cell',
|
||||
props: {
|
||||
date: {
|
||||
type: Date,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="label-wrapper">
|
||||
<span class="tag" v-for="label in labels" :style="{'background': label.hex_color, 'color': label.textColor}" :key="label.id">
|
||||
<span>{{ label.title }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'labels',
|
||||
props: {
|
||||
labels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.label-wrapper {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,11 @@
|
|||
<template>
|
||||
<span v-if="priority >= priorities.HIGH" class="high-priority" :class="{'not-so-high': priority === priorities.HIGH}">
|
||||
<span class="icon">
|
||||
<span v-if="showAll || priority >= priorities.HIGH" :class="{'not-so-high': priority === priorities.HIGH, 'high-priority': priority >= priorities.HIGH}">
|
||||
<span class="icon" v-if="priority >= priorities.HIGH">
|
||||
<icon icon="exclamation"/>
|
||||
</span>
|
||||
<template v-if="priority === priorities.UNSET">Unset</template>
|
||||
<template v-if="priority === priorities.LOW">Low</template>
|
||||
<template v-if="priority === priorities.MEDIUM">Medium</template>
|
||||
<template v-if="priority === priorities.HIGH">High</template>
|
||||
<template v-if="priority === priorities.URGENT">Urgent</template>
|
||||
<template v-if="priority === priorities.DO_NOW">DO NOW</template>
|
||||
|
@ -26,7 +29,11 @@
|
|||
priority: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
}
|
||||
},
|
||||
showAll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
<template>
|
||||
<span>
|
||||
<div class="fancycheckbox" :class="{'is-disabled': isArchived}">
|
||||
<input @change="markAsDone" type="checkbox" :id="task.id" :checked="task.done"
|
||||
style="display: none;" :disabled="isArchived">
|
||||
<label :for="task.id" class="check">
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18">
|
||||
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
||||
<polyline points="1 9 7 14 15 4"></polyline>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<fancycheckbox v-model="task.done" @change="markAsDone" :disabled="isArchived"/>
|
||||
<router-link :to="{ name: 'taskDetailView', params: { id: task.id } }" class="tasktext" :class="{ 'done': task.done}">
|
||||
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
||||
<span class="parent-tasks" v-if="typeof task.related_tasks.parenttask !== 'undefined'">
|
||||
|
@ -19,18 +10,15 @@
|
|||
>
|
||||
</span>
|
||||
{{ task.text }}
|
||||
<span class="tag" v-for="label in task.labels" :style="{'background': label.hex_color, 'color': label.textColor}"
|
||||
:key="label.id">
|
||||
<span>{{ label.title }}</span>
|
||||
</span>
|
||||
<img
|
||||
:src="a.getAvatarUrl(27)"
|
||||
:alt="a.username"
|
||||
class="avatar"
|
||||
width="27"
|
||||
height="27"
|
||||
<labels :labels="task.labels"/>
|
||||
<user
|
||||
:user="a"
|
||||
:avatar-size="27"
|
||||
:show-username="false"
|
||||
:is-inline="true"
|
||||
v-for="(a, i) in task.assignees"
|
||||
:key="task.id + 'assignee' + a.id + i"/>
|
||||
:key="task.id + 'assignee' + a.id + i"
|
||||
/>
|
||||
<i v-if="task.dueDate > 0"
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
v-tooltip="formatDate(task.dueDate)"> - Due {{formatDateSince(task.dueDate)}}</i>
|
||||
|
@ -43,6 +31,9 @@
|
|||
import TaskModel from '../../../models/task'
|
||||
import PriorityLabel from './priorityLabel'
|
||||
import TaskService from '../../../services/task'
|
||||
import Labels from './labels'
|
||||
import User from '../../global/user'
|
||||
import Fancycheckbox from '../../global/fancycheckbox'
|
||||
|
||||
export default {
|
||||
name: 'singleTaskInList',
|
||||
|
@ -53,6 +44,9 @@
|
|||
}
|
||||
},
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
User,
|
||||
Labels,
|
||||
PriorityLabel,
|
||||
},
|
||||
props: {
|
||||
|
@ -78,10 +72,8 @@
|
|||
this.taskService = new TaskService()
|
||||
},
|
||||
methods: {
|
||||
markAsDone(e) {
|
||||
let updateFunc = () => {
|
||||
// We get the task, update the 'done' property and then push it to the api.
|
||||
this.task.done = e.target.checked
|
||||
markAsDone(checked) {
|
||||
const updateFunc = () => {
|
||||
this.taskService.update(this.task)
|
||||
.then(t => {
|
||||
this.task = t
|
||||
|
@ -93,8 +85,7 @@
|
|||
title: 'Undo',
|
||||
callback: () => this.markAsDone({
|
||||
target: {
|
||||
id: e.target.id,
|
||||
checked: !e.target.checked
|
||||
checked: !checked
|
||||
}
|
||||
}),
|
||||
}]
|
||||
|
@ -105,8 +96,8 @@
|
|||
})
|
||||
}
|
||||
|
||||
if (e.target.checked) {
|
||||
setTimeout(updateFunc(), 300); // Delay it to show the animation when marking a task as done
|
||||
if (checked) {
|
||||
setTimeout(updateFunc, 300); // Delay it to show the animation when marking a task as done
|
||||
} else {
|
||||
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<a @click="click">
|
||||
<icon icon="sort-up" v-if="order === 'asc'"/>
|
||||
<icon icon="sort-up" v-else-if="order === 'desc'" rotation="180"/>
|
||||
<icon icon="sort" v-else/>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'sort',
|
||||
props: {
|
||||
order: {
|
||||
type: String,
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -64,6 +64,9 @@ import { faClock } from '@fortawesome/free-regular-svg-icons'
|
|||
import { faHistory } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCheckDouble } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSort } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSortUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faComments } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
|
@ -104,6 +107,9 @@ library.add(faHistory)
|
|||
library.add(faSearch)
|
||||
library.add(faCheckDouble)
|
||||
library.add(faComments)
|
||||
library.add(faTh)
|
||||
library.add(faSort)
|
||||
library.add(faSortUp)
|
||||
|
||||
Vue.component('icon', FontAwesomeIcon)
|
||||
|
||||
|
|
|
@ -13,3 +13,4 @@
|
|||
@import 'teams';
|
||||
@import 'migrator';
|
||||
@import 'comments';
|
||||
@import 'table-view';
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
span {
|
||||
font-size: 0.8em;
|
||||
vertical-align: top;
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.table-view {
|
||||
.table {
|
||||
background: transparent;
|
||||
|
||||
.user {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.is-done {
|
||||
font-size: .9em;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.column-filter {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
margin-top: -80px;
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.fancycheckbox {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,16 +55,6 @@
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.is-done {
|
||||
background: $green;
|
||||
color: $white;
|
||||
padding: .5em;
|
||||
font-size: 1.5em;
|
||||
margin-left: .5em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.date-input {
|
||||
|
@ -181,3 +171,15 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-done {
|
||||
background: $green;
|
||||
color: $white;
|
||||
padding: .5em;
|
||||
font-size: 1.5em;
|
||||
margin-left: .5em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
Reference in New Issue