Table View for tasks #76

Merged
konrad merged 23 commits from feature/table-view into master 2020-04-01 20:13:57 +00:00
21 changed files with 623 additions and 197 deletions

View File

@ -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,

View File

@ -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>

View File

@ -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%;

View File

@ -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">

View File

@ -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,

View File

@ -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 }})
}
},

View File

@ -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,
},

View File

@ -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
},

View File

@ -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">&hellip;</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>

View File

@ -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&nbsp;Date
<sort :order="sortBy.due_date_unix" @click="sort('due_date_unix')"/>
</th>
<th v-if="activeColumns.startDate">
Start&nbsp;Date
<sort :order="sortBy.start_date_unix" @click="sort('start_date_unix')"/>
</th>
<th v-if="activeColumns.endDate">
End&nbsp;Date
<sort :order="sortBy.end_date_unix" @click="sort('end_date_unix')"/>
</th>
<th v-if="activeColumns.percentDone">
%&nbsp;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&nbsp;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">&hellip;</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>

View File

@ -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,
},
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -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>

View File

@ -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)

View File

@ -13,3 +13,4 @@
@import 'teams';
@import 'migrator';
@import 'comments';
@import 'table-view';

View File

@ -28,6 +28,7 @@
span {
font-size: 0.8em;
vertical-align: top;
padding-left: .5em;
}
svg {

View File

@ -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;
}
}
}

View File

@ -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;
}