Cleanup code & make sure it has a common code style
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="loader-container edit-list is-max-width-desktop" :class="{ 'is-loading': listService.loading}">
|
||||
<div :class="{ 'is-loading': listService.loading}" class="loader-container edit-list is-max-width-desktop">
|
||||
<div class="notification is-warning" v-if="list.isArchived">
|
||||
This list is archived.
|
||||
It is not possible to create new or edit tasks or it.
|
||||
@ -17,47 +17,47 @@
|
||||
<label class="label" for="listtext">List Name</label>
|
||||
<div class="control">
|
||||
<input
|
||||
v-focus
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
:disabled="listService.loading"
|
||||
class="input"
|
||||
type="text"
|
||||
id="listtext"
|
||||
placeholder="The list title goes here..."
|
||||
@keyup.enter="submit"
|
||||
v-model="list.title"/>
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
:disabled="listService.loading"
|
||||
@keyup.enter="submit"
|
||||
class="input"
|
||||
id="listtext"
|
||||
placeholder="The list title goes here..."
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="list.title"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
class="label"
|
||||
for="listtext"
|
||||
v-tooltip="'The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.'">
|
||||
class="label"
|
||||
for="listtext"
|
||||
v-tooltip="'The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.'">
|
||||
List Identifier
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
v-focus
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
:disabled="listService.loading"
|
||||
class="input"
|
||||
type="text"
|
||||
id="listtext"
|
||||
placeholder="The list identifier goes here..."
|
||||
@keyup.enter="submit"
|
||||
v-model="list.identifier"/>
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
:disabled="listService.loading"
|
||||
@keyup.enter="submit"
|
||||
class="input"
|
||||
id="listtext"
|
||||
placeholder="The list identifier goes here..."
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="list.identifier"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="listdescription">Description</label>
|
||||
<div class="control">
|
||||
<editor
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
:disabled="listService.loading"
|
||||
placeholder="The lists description goes here..."
|
||||
id="listdescription"
|
||||
v-model="list.description"
|
||||
:preview-is-default="false"
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
:disabled="listService.loading"
|
||||
:preview-is-default="false"
|
||||
id="listdescription"
|
||||
placeholder="The lists description goes here..."
|
||||
v-model="list.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,8 +65,8 @@
|
||||
<label class="label" for="isArchivedCheck">Is Archived</label>
|
||||
<div class="control">
|
||||
<fancycheckbox
|
||||
v-model="list.isArchived"
|
||||
v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
|
||||
v-model="list.isArchived"
|
||||
v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
|
||||
This list is archived
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
@ -81,14 +81,14 @@
|
||||
|
||||
<div class="columns bigbuttons">
|
||||
<div class="column">
|
||||
<button @click="submit()" class="button is-primary is-fullwidth"
|
||||
:class="{ 'is-loading': listService.loading}">
|
||||
<button :class="{ 'is-loading': listService.loading}" @click="submit()"
|
||||
class="button is-primary is-fullwidth">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<button @click="showDeleteModal = true" class="button is-danger is-fullwidth"
|
||||
:class="{ 'is-loading': listService.loading}">
|
||||
<button :class="{ 'is-loading': listService.loading}" @click="showDeleteModal = true"
|
||||
class="button is-danger is-fullwidth">
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
@ -114,10 +114,10 @@
|
||||
</p>
|
||||
<p class="control">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-success"
|
||||
@click="duplicateList"
|
||||
:class="{'is-loading': listDuplicateService.loading}">
|
||||
:class="{'is-loading': listDuplicateService.loading}"
|
||||
@click="duplicateList"
|
||||
class="button is-success"
|
||||
type="submit">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
@ -132,24 +132,24 @@
|
||||
<background :list-id="$route.params.id"/>
|
||||
|
||||
<component
|
||||
:is="manageUsersComponent"
|
||||
:id="list.id"
|
||||
type="list"
|
||||
shareType="user"
|
||||
:userIsAdmin="userIsAdmin"/>
|
||||
:id="list.id"
|
||||
:is="manageUsersComponent"
|
||||
:userIsAdmin="userIsAdmin"
|
||||
shareType="user"
|
||||
type="list"/>
|
||||
<component
|
||||
:is="manageTeamsComponent"
|
||||
:id="list.id"
|
||||
type="list"
|
||||
shareType="team"
|
||||
:userIsAdmin="userIsAdmin"/>
|
||||
:id="list.id"
|
||||
:is="manageTeamsComponent"
|
||||
:userIsAdmin="userIsAdmin"
|
||||
shareType="team"
|
||||
type="list"/>
|
||||
|
||||
<link-sharing :list-id="$route.params.id" v-if="linkSharingEnabled"/>
|
||||
|
||||
<modal
|
||||
v-if="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteList()">
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteList()"
|
||||
v-if="showDeleteModal">
|
||||
<span slot="header">Delete the list</span>
|
||||
<p slot="text">Are you sure you want to delete this list and all of its contents?
|
||||
<br/>This includes all tasks and <b>CANNOT BE UNDONE!</b></p>
|
||||
@ -158,123 +158,123 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../../router'
|
||||
import manageSharing from '../../components/sharing/userTeam'
|
||||
import LinkSharing from '../../components/sharing/linkSharing'
|
||||
import router from '../../router'
|
||||
import manageSharing from '../../components/sharing/userTeam'
|
||||
import LinkSharing from '../../components/sharing/linkSharing'
|
||||
|
||||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import Fancycheckbox from '../../components/input/fancycheckbox'
|
||||
import Background from '../../components/list/partials/background-settings'
|
||||
import {CURRENT_LIST} from '../../store/mutation-types'
|
||||
import ColorPicker from '../../components/input/colorPicker'
|
||||
import NamespaceSearch from '../../components/namespace/namespace-search'
|
||||
import ListDuplicateService from '../../services/listDuplicateService'
|
||||
import ListDuplicateModel from '../../models/listDuplicateModel'
|
||||
import LoadingComponent from '../../components/misc/loading'
|
||||
import ErrorComponent from '../../components/misc/error'
|
||||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import Fancycheckbox from '../../components/input/fancycheckbox'
|
||||
import Background from '../../components/list/partials/background-settings'
|
||||
import {CURRENT_LIST} from '@/store/mutation-types'
|
||||
import ColorPicker from '../../components/input/colorPicker'
|
||||
import NamespaceSearch from '../../components/namespace/namespace-search'
|
||||
import ListDuplicateService from '../../services/listDuplicateService'
|
||||
import ListDuplicateModel from '../../models/listDuplicateModel'
|
||||
import LoadingComponent from '../../components/misc/loading'
|
||||
import ErrorComponent from '../../components/misc/error'
|
||||
|
||||
export default {
|
||||
name: 'EditList',
|
||||
data() {
|
||||
return {
|
||||
list: ListModel,
|
||||
listService: ListService,
|
||||
export default {
|
||||
name: 'EditList',
|
||||
data() {
|
||||
return {
|
||||
list: ListModel,
|
||||
listService: ListService,
|
||||
|
||||
showDeleteModal: false,
|
||||
showDeleteModal: false,
|
||||
|
||||
manageUsersComponent: '',
|
||||
manageTeamsComponent: '',
|
||||
manageUsersComponent: '',
|
||||
manageTeamsComponent: '',
|
||||
|
||||
listDuplicateService: ListDuplicateService,
|
||||
selectedNamespace: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
NamespaceSearch,
|
||||
ColorPicker,
|
||||
Background,
|
||||
Fancycheckbox,
|
||||
LinkSharing,
|
||||
manageSharing,
|
||||
editor: () => ({
|
||||
component: import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ '../../components/input/editor'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.listDuplicateService = new ListDuplicateService()
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes
|
||||
'$route': 'loadList'
|
||||
},
|
||||
computed: {
|
||||
linkSharingEnabled() {
|
||||
return this.$store.state.config.linkSharingEnabled
|
||||
},
|
||||
userIsAdmin() {
|
||||
return this.list.owner && this.list.owner.id === this.$store.state.auth.info.id
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadList() {
|
||||
let list = new ListModel({id: this.$route.params.id})
|
||||
this.listService.get(list)
|
||||
.then(r => {
|
||||
this.$set(this, 'list', r)
|
||||
this.$store.commit(CURRENT_LIST, r)
|
||||
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
|
||||
this.manageTeamsComponent = 'manageSharing'
|
||||
this.manageUsersComponent = 'manageSharing'
|
||||
this.setTitle(`Edit ${this.list.title}`)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
submit() {
|
||||
this.listService.update(this.list)
|
||||
.then(r => {
|
||||
this.$store.commit('namespaces/setListInNamespaceById', r)
|
||||
this.success({message: 'The list was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteList() {
|
||||
this.listService.delete(this.list)
|
||||
.then(() => {
|
||||
this.success({message: 'The list was successfully deleted.'}, this)
|
||||
router.push({name: 'home'})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
selectNamespace(namespace) {
|
||||
this.selectedNamespace = namespace
|
||||
},
|
||||
duplicateList() {
|
||||
const listDuplicate = new ListDuplicateModel({
|
||||
listId: this.list.id,
|
||||
namespaceId: this.selectedNamespace.id,
|
||||
})
|
||||
this.listDuplicateService.create(listDuplicate)
|
||||
.then(r => {
|
||||
this.$store.commit('namespaces/addListToNamespace', r.list)
|
||||
this.success({message: 'The list was successfully duplicated.'}, this)
|
||||
router.push({name: 'list.index', params: {listId: r.list.id}})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
listDuplicateService: ListDuplicateService,
|
||||
selectedNamespace: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
NamespaceSearch,
|
||||
ColorPicker,
|
||||
Background,
|
||||
Fancycheckbox,
|
||||
LinkSharing,
|
||||
manageSharing,
|
||||
editor: () => ({
|
||||
component: import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ '../../components/input/editor'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.listDuplicateService = new ListDuplicateService()
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes
|
||||
'$route': 'loadList',
|
||||
},
|
||||
computed: {
|
||||
linkSharingEnabled() {
|
||||
return this.$store.state.config.linkSharingEnabled
|
||||
},
|
||||
userIsAdmin() {
|
||||
return this.list.owner && this.list.owner.id === this.$store.state.auth.info.id
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadList() {
|
||||
let list = new ListModel({id: this.$route.params.id})
|
||||
this.listService.get(list)
|
||||
.then(r => {
|
||||
this.$set(this, 'list', r)
|
||||
this.$store.commit(CURRENT_LIST, r)
|
||||
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
|
||||
this.manageTeamsComponent = 'manageSharing'
|
||||
this.manageUsersComponent = 'manageSharing'
|
||||
this.setTitle(`Edit ${this.list.title}`)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
submit() {
|
||||
this.listService.update(this.list)
|
||||
.then(r => {
|
||||
this.$store.commit('namespaces/setListInNamespaceById', r)
|
||||
this.success({message: 'The list was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteList() {
|
||||
this.listService.delete(this.list)
|
||||
.then(() => {
|
||||
this.success({message: 'The list was successfully deleted.'}, this)
|
||||
router.push({name: 'home'})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
selectNamespace(namespace) {
|
||||
this.selectedNamespace = namespace
|
||||
},
|
||||
duplicateList() {
|
||||
const listDuplicate = new ListDuplicateModel({
|
||||
listId: this.list.id,
|
||||
namespaceId: this.selectedNamespace.id,
|
||||
})
|
||||
this.listDuplicateService.create(listDuplicate)
|
||||
.then(r => {
|
||||
this.$store.commit('namespaces/addListToNamespace', r.list)
|
||||
this.success({message: 'The list was successfully duplicated.'}, this)
|
||||
router.push({name: 'list.index', params: {listId: r.list.id}})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,23 +1,24 @@
|
||||
<template>
|
||||
<div class="fullpage">
|
||||
<a class="close" @click="back()">
|
||||
<a @click="back()" class="close">
|
||||
<icon :icon="['far', 'times-circle']">
|
||||
</icon>
|
||||
</a>
|
||||
<h3>Create a new list</h3>
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded" :class="{ 'is-loading': listService.loading}">
|
||||
<input v-focus
|
||||
class="input"
|
||||
<p :class="{ 'is-loading': listService.loading}" class="control is-expanded">
|
||||
<input
|
||||
:class="{ 'disabled': listService.loading}"
|
||||
v-model="list.title"
|
||||
type="text"
|
||||
placeholder="The list's name goes here..."
|
||||
@keyup.enter="newList()"
|
||||
@keyup.esc="back()"
|
||||
@keyup.enter="newList()"/>
|
||||
class="input"
|
||||
placeholder="The list's name goes here..."
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="list.title"/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-success noshadow" @click="newList()" :disabled="list.title === ''">
|
||||
<button :disabled="list.title === ''" @click="newList()" class="button is-success noshadow">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
@ -32,51 +33,51 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../../router'
|
||||
import ListService from '../../services/list'
|
||||
import ListModel from '../../models/list'
|
||||
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||
import router from '../../router'
|
||||
import ListService from '../../services/list'
|
||||
import ListModel from '../../models/list'
|
||||
import {IS_FULLPAGE} from '@/store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: "NewList",
|
||||
data() {
|
||||
return {
|
||||
showError: false,
|
||||
list: ListModel,
|
||||
listService: ListService,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.list = new ListModel()
|
||||
this.listService = new ListService()
|
||||
this.$store.commit(IS_FULLPAGE, true)
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle('Create a new list')
|
||||
},
|
||||
methods: {
|
||||
newList() {
|
||||
if (this.list.title === '') {
|
||||
this.showError = true
|
||||
return
|
||||
}
|
||||
this.showError = false
|
||||
|
||||
this.list.namespaceId = this.$route.params.id
|
||||
this.listService.create(this.list)
|
||||
.then(response => {
|
||||
response.namespaceId = this.list.namespaceId
|
||||
this.$store.commit('namespaces/addListToNamespace', response)
|
||||
this.success({message: 'The list was successfully created.'}, this)
|
||||
router.push({name: 'list.index', params: {listId: response.id}})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
back() {
|
||||
router.go(-1)
|
||||
},
|
||||
export default {
|
||||
name: 'NewList',
|
||||
data() {
|
||||
return {
|
||||
showError: false,
|
||||
list: ListModel,
|
||||
listService: ListService,
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.list = new ListModel()
|
||||
this.listService = new ListService()
|
||||
this.$store.commit(IS_FULLPAGE, true)
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle('Create a new list')
|
||||
},
|
||||
methods: {
|
||||
newList() {
|
||||
if (this.list.title === '') {
|
||||
this.showError = true
|
||||
return
|
||||
}
|
||||
this.showError = false
|
||||
|
||||
this.list.namespaceId = this.$route.params.id
|
||||
this.listService.create(this.list)
|
||||
.then(response => {
|
||||
response.namespaceId = this.list.namespaceId
|
||||
this.$store.commit('namespaces/addListToNamespace', response)
|
||||
this.success({message: 'The list was successfully created.'}, this)
|
||||
router.push({name: 'list.index', params: {listId: response.id}})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
back() {
|
||||
router.go(-1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,27 +1,27 @@
|
||||
<template>
|
||||
<div
|
||||
class="loader-container"
|
||||
:class="{ 'is-loading': listService.loading}"
|
||||
:class="{ 'is-loading': listService.loading}"
|
||||
class="loader-container"
|
||||
>
|
||||
<div class="switch-view">
|
||||
<router-link
|
||||
:to="{ name: 'list.list', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.list'}">
|
||||
:class="{'is-active': $route.name === 'list.list'}"
|
||||
:to="{ name: 'list.list', params: { listId: listId } }">
|
||||
List
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'list.gantt', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.gantt'}">
|
||||
:class="{'is-active': $route.name === 'list.gantt'}"
|
||||
:to="{ name: 'list.gantt', params: { listId: listId } }">
|
||||
Gantt
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'list.table', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.table'}">
|
||||
:class="{'is-active': $route.name === 'list.table'}"
|
||||
:to="{ name: 'list.table', params: { listId: listId } }">
|
||||
Table
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'list.kanban', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.kanban'}">
|
||||
:class="{'is-active': $route.name === 'list.kanban'}"
|
||||
:to="{ name: 'list.kanban', params: { listId: listId } }">
|
||||
Kanban
|
||||
</router-link>
|
||||
</div>
|
||||
@ -35,107 +35,110 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../../router'
|
||||
import router from '../../router'
|
||||
|
||||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import {CURRENT_LIST} from '../../store/mutation-types'
|
||||
import {getListView} from '../../helpers/saveListView'
|
||||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import {CURRENT_LIST} from '@/store/mutation-types'
|
||||
import {getListView} from '@/helpers/saveListView'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listService: ListService,
|
||||
list: ListModel,
|
||||
listLoaded: 0,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.list = new ListModel()
|
||||
},
|
||||
mounted() {
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes
|
||||
'$route.path': 'loadList',
|
||||
},
|
||||
computed: {
|
||||
// Computed property to let "listId" always have a value
|
||||
listId() {
|
||||
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
|
||||
},
|
||||
background() {
|
||||
return this.$store.state.background
|
||||
},
|
||||
currentList() {
|
||||
return typeof this.$store.state.currentList === 'undefined' ? {id: 0, title: ''} : this.$store.state.currentList
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
replaceListView() {
|
||||
const savedListView = getListView(this.$route.params.listId)
|
||||
router.replace({name: savedListView, params: {id: this.$route.params.listId}})
|
||||
console.debug('Replaced list view with ', savedListView)
|
||||
return
|
||||
},
|
||||
loadList() {
|
||||
this.setTitle(this.currentList.title)
|
||||
|
||||
// This invalidates the loaded list at the kanban board which lets it reload its content when
|
||||
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
|
||||
// shown in all views while preventing reloads when closing a task popup.
|
||||
// We don't do this for the table view because that does not change tasks.
|
||||
if (
|
||||
this.$route.name === 'list.list' ||
|
||||
this.$route.name === 'list.gantt'
|
||||
) {
|
||||
this.$store.commit('kanban/setListId', 0)
|
||||
}
|
||||
|
||||
// When clicking again on a list in the menu, there would be no list view selected which means no list
|
||||
// at all. Users will then have to click on the list view menu again which is quite confusing.
|
||||
if (this.$route.name === 'list.index') {
|
||||
return this.replaceListView()
|
||||
}
|
||||
|
||||
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently
|
||||
if (
|
||||
this.$route.params.listId === this.listLoaded ||
|
||||
typeof this.$route.params.listId === 'undefined' ||
|
||||
this.$route.params.listId === this.currentList.id ||
|
||||
parseInt(this.$route.params.listId) === this.currentList.id
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect the user to list view by default
|
||||
if (
|
||||
this.$route.name !== 'list.list' &&
|
||||
this.$route.name !== 'list.gantt' &&
|
||||
this.$route.name !== 'list.table' &&
|
||||
this.$route.name !== 'list.kanban'
|
||||
) {
|
||||
return this.replaceListView()
|
||||
}
|
||||
|
||||
console.debug(`Loading list, $route.name = ${this.$route.name}, $route.params =`, this.$route.params, `, listLoaded = ${this.listLoaded}, currentList = `, this.currentList)
|
||||
|
||||
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
|
||||
let list = new ListModel({id: this.$route.params.listId})
|
||||
this.listService.get(list)
|
||||
.then(r => {
|
||||
this.$set(this, 'list', r)
|
||||
this.$store.commit(CURRENT_LIST, r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.listLoaded = this.$route.params.listId
|
||||
})
|
||||
},
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listService: ListService,
|
||||
list: ListModel,
|
||||
listLoaded: 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.list = new ListModel()
|
||||
},
|
||||
mounted() {
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes
|
||||
'$route.path': 'loadList',
|
||||
},
|
||||
computed: {
|
||||
// Computed property to let "listId" always have a value
|
||||
listId() {
|
||||
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
|
||||
},
|
||||
background() {
|
||||
return this.$store.state.background
|
||||
},
|
||||
currentList() {
|
||||
return typeof this.$store.state.currentList === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
} : this.$store.state.currentList
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
replaceListView() {
|
||||
const savedListView = getListView(this.$route.params.listId)
|
||||
router.replace({name: savedListView, params: {id: this.$route.params.listId}})
|
||||
console.debug('Replaced list view with ', savedListView)
|
||||
return
|
||||
},
|
||||
loadList() {
|
||||
this.setTitle(this.currentList.title)
|
||||
|
||||
// This invalidates the loaded list at the kanban board which lets it reload its content when
|
||||
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
|
||||
// shown in all views while preventing reloads when closing a task popup.
|
||||
// We don't do this for the table view because that does not change tasks.
|
||||
if (
|
||||
this.$route.name === 'list.list' ||
|
||||
this.$route.name === 'list.gantt'
|
||||
) {
|
||||
this.$store.commit('kanban/setListId', 0)
|
||||
}
|
||||
|
||||
// When clicking again on a list in the menu, there would be no list view selected which means no list
|
||||
// at all. Users will then have to click on the list view menu again which is quite confusing.
|
||||
if (this.$route.name === 'list.index') {
|
||||
return this.replaceListView()
|
||||
}
|
||||
|
||||
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently
|
||||
if (
|
||||
this.$route.params.listId === this.listLoaded ||
|
||||
typeof this.$route.params.listId === 'undefined' ||
|
||||
this.$route.params.listId === this.currentList.id ||
|
||||
parseInt(this.$route.params.listId) === this.currentList.id
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect the user to list view by default
|
||||
if (
|
||||
this.$route.name !== 'list.list' &&
|
||||
this.$route.name !== 'list.gantt' &&
|
||||
this.$route.name !== 'list.table' &&
|
||||
this.$route.name !== 'list.kanban'
|
||||
) {
|
||||
return this.replaceListView()
|
||||
}
|
||||
|
||||
console.debug(`Loading list, $route.name = ${this.$route.name}, $route.params =`, this.$route.params, `, listLoaded = ${this.listLoaded}, currentList = `, this.currentList)
|
||||
|
||||
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
|
||||
let list = new ListModel({id: this.$route.params.listId})
|
||||
this.listService.get(list)
|
||||
.then(r => {
|
||||
this.$set(this, 'list', r)
|
||||
this.$store.commit(CURRENT_LIST, r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.listLoaded = this.$route.params.listId
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="gantt-chart-container">
|
||||
<div class="gantt-options">
|
||||
<fancycheckbox v-model="showTaskswithoutDates" class="is-block">
|
||||
<fancycheckbox class="is-block" v-model="showTaskswithoutDates">
|
||||
Show tasks which don't have dates set
|
||||
</fancycheckbox>
|
||||
<div class="range-picker">
|
||||
@ -21,11 +21,11 @@
|
||||
<label class="label" for="fromDate">From</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
class="input"
|
||||
v-model="dateFrom"
|
||||
:config="flatPickerConfig"
|
||||
class="input"
|
||||
id="fromDate"
|
||||
placeholder="From"
|
||||
v-model="dateFrom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,22 +33,22 @@
|
||||
<label class="label" for="toDate">To</label>
|
||||
<div class="control">
|
||||
<flat-pickr
|
||||
class="input"
|
||||
v-model="dateTo"
|
||||
:config="flatPickerConfig"
|
||||
class="input"
|
||||
id="toDate"
|
||||
placeholder="To"
|
||||
v-model="dateTo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gantt-chart
|
||||
:list-id="Number($route.params.listId)"
|
||||
:show-taskswithout-dates="showTaskswithoutDates"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
:day-width="dayWidth"
|
||||
:list-id="Number($route.params.listId)"
|
||||
:show-taskswithout-dates="showTaskswithoutDates"
|
||||
/>
|
||||
|
||||
<!-- This router view is used to show the task popup while keeping the gantt chart itself -->
|
||||
@ -60,40 +60,40 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GanttChart from '../../../components/tasks/gantt-component'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import Fancycheckbox from '../../../components/input/fancycheckbox'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import GanttChart from '../../../components/tasks/gantt-component'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import Fancycheckbox from '../../../components/input/fancycheckbox'
|
||||
import {saveListView} from '@/helpers/saveListView'
|
||||
|
||||
export default {
|
||||
name: 'Gantt',
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
flatPickr,
|
||||
GanttChart
|
||||
},
|
||||
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)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTaskswithoutDates: false,
|
||||
dayWidth: 35,
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
flatPickerConfig:{
|
||||
altFormat: 'j M Y',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
enableTime: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.dateFrom = new Date((new Date()).setDate((new Date()).getDate() - 15))
|
||||
this.dateTo = new Date((new Date()).setDate((new Date()).getDate() + 30))
|
||||
},
|
||||
}
|
||||
export default {
|
||||
name: 'Gantt',
|
||||
components: {
|
||||
Fancycheckbox,
|
||||
flatPickr,
|
||||
GanttChart,
|
||||
},
|
||||
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)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTaskswithoutDates: false,
|
||||
dayWidth: 35,
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
flatPickerConfig: {
|
||||
altFormat: 'j M Y',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
enableTime: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.dateFrom = new Date((new Date()).setDate((new Date()).getDate() - 15))
|
||||
this.dateTo = new Date((new Date()).setDate((new Date()).getDate() + 30))
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div class="kanban loader-container" :class="{ 'is-loading': loading}">
|
||||
<div v-for="bucket in buckets" :key="`bucket${bucket.id}`" class="bucket">
|
||||
<div :class="{ 'is-loading': loading}" class="kanban loader-container">
|
||||
<div :key="`bucket${bucket.id}`" class="bucket" v-for="bucket in buckets">
|
||||
<div class="bucket-header">
|
||||
<h2
|
||||
class="title input"
|
||||
contenteditable="true"
|
||||
spellcheck="false"
|
||||
@focusout="() => saveBucketTitle(bucket.id)"
|
||||
:ref="`bucket${bucket.id}title`"
|
||||
@keyup.ctrl.enter="() => saveBucketTitle(bucket.id)">{{ bucket.title }}</h2>
|
||||
<span
|
||||
class="limit"
|
||||
:ref="`bucket${bucket.id}title`"
|
||||
@focusout="() => saveBucketTitle(bucket.id)"
|
||||
@keyup.ctrl.enter="() => saveBucketTitle(bucket.id)"
|
||||
class="title input"
|
||||
contenteditable="true"
|
||||
spellcheck="false">{{ bucket.title }}</h2>
|
||||
<span
|
||||
:class="{'is-max': bucket.tasks.length >= bucket.limit}"
|
||||
class="limit"
|
||||
v-if="bucket.limit > 0">
|
||||
{{ bucket.tasks.length }}/{{ bucket.limit }}
|
||||
</span>
|
||||
<div
|
||||
class="dropdown is-right options"
|
||||
:class="{ 'is-active': bucketOptionsDropDownActive[bucket.id] }"
|
||||
v-if="canWrite"
|
||||
:class="{ 'is-active': bucketOptionsDropDownActive[bucket.id] }"
|
||||
class="dropdown is-right options"
|
||||
v-if="canWrite"
|
||||
>
|
||||
<div class="dropdown-trigger" @click.stop="toggleBucketDropdown(bucket.id)">
|
||||
<div @click.stop="toggleBucketDropdown(bucket.id)" class="dropdown-trigger">
|
||||
<span class="icon">
|
||||
<icon icon="ellipsis-v"/>
|
||||
</span>
|
||||
@ -28,18 +28,18 @@
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click.stop="showSetLimitInput = true"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<div class="field has-addons" v-if="showSetLimitInput">
|
||||
<div class="control">
|
||||
<input
|
||||
type="number"
|
||||
@change="() => updateBucket(bucket)"
|
||||
@keyup.enter="() => updateBucket(bucket)"
|
||||
class="input"
|
||||
type="number"
|
||||
v-focus.always
|
||||
v-model="bucket.limit"
|
||||
@keyup.enter="() => updateBucket(bucket)"
|
||||
@change="() => updateBucket(bucket)"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
@ -55,10 +55,10 @@
|
||||
</template>
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item has-text-danger"
|
||||
@click="() => deleteBucketModal(bucket.id)"
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
v-tooltip="buckets.length <= 1 ? 'You cannot remove the last bucket.' : ''"
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
@click="() => deleteBucketModal(bucket.id)"
|
||||
class="dropdown-item has-text-danger"
|
||||
v-tooltip="buckets.length <= 1 ? 'You cannot remove the last bucket.' : ''"
|
||||
>
|
||||
<span class="icon is-small"><icon icon="trash-alt"/></span>
|
||||
Delete
|
||||
@ -67,37 +67,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tasks" :ref="`tasks-container${bucket.id}`">
|
||||
<div :ref="`tasks-container${bucket.id}`" class="tasks">
|
||||
<!-- Make the component either a div or a draggable component based on the user rights -->
|
||||
<component
|
||||
:is="canWrite ? 'Container' : 'div'"
|
||||
@drop="e => onDrop(bucket.id, e)"
|
||||
group-name="buckets"
|
||||
:get-child-payload="getTaskPayload(bucket.id)"
|
||||
:drop-placeholder="dropPlaceholderOptions"
|
||||
:animation-duration="150"
|
||||
:should-accept-drop="() => shouldAcceptDrop(bucket)"
|
||||
drag-class="ghost-task"
|
||||
drag-class-drop="ghost-task-drop"
|
||||
drag-handle-selector=".task.draggable"
|
||||
:animation-duration="150"
|
||||
:drop-placeholder="dropPlaceholderOptions"
|
||||
:get-child-payload="getTaskPayload(bucket.id)"
|
||||
:is="canWrite ? 'Container' : 'div'"
|
||||
:should-accept-drop="() => shouldAcceptDrop(bucket)"
|
||||
@drop="e => onDrop(bucket.id, e)"
|
||||
drag-class="ghost-task"
|
||||
drag-class-drop="ghost-task-drop"
|
||||
drag-handle-selector=".task.draggable"
|
||||
group-name="buckets"
|
||||
>
|
||||
<!-- Make the component either a div or a draggable component based on the user rights -->
|
||||
<component
|
||||
v-for="task in bucket.tasks"
|
||||
:key="`bucket${bucket.id}-task${task.id}`"
|
||||
:is="canWrite ? 'Draggable' : 'div'"
|
||||
:is="canWrite ? 'Draggable' : 'div'"
|
||||
:key="`bucket${bucket.id}-task${task.id}`"
|
||||
v-for="task in bucket.tasks"
|
||||
>
|
||||
<div
|
||||
class="task loader-container draggable"
|
||||
:class="{
|
||||
:class="{
|
||||
'is-loading': taskService.loading && taskUpdating[task.id],
|
||||
'draggable': !taskService.loading || !taskUpdating[task.id],
|
||||
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
|
||||
}"
|
||||
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
|
||||
@click.ctrl="() => markTaskAsDone(task)"
|
||||
@click.meta="() => markTaskAsDone(task)"
|
||||
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
|
||||
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
|
||||
@click.ctrl="() => markTaskAsDone(task)"
|
||||
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
|
||||
@click.meta="() => markTaskAsDone(task)"
|
||||
class="task loader-container draggable"
|
||||
>
|
||||
<span class="task-id">
|
||||
<span class="is-done" v-if="task.done">Done</span>
|
||||
@ -109,10 +109,10 @@
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
v-if="task.dueDate > 0"
|
||||
class="due-date"
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
v-tooltip="formatDate(task.dueDate)">
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="due-date"
|
||||
v-if="task.dueDate > 0"
|
||||
v-tooltip="formatDate(task.dueDate)">
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
@ -127,22 +127,22 @@
|
||||
<priority-label :priority="task.priority" class="priority-label"/>
|
||||
<div class="assignees" v-if="task.assignees.length > 0">
|
||||
<user
|
||||
v-for="u in task.assignees"
|
||||
:key="task.id + 'assignee' + u.id"
|
||||
:user="u"
|
||||
:show-username="false"
|
||||
:avatar-size="24"
|
||||
:avatar-size="24"
|
||||
:key="task.id + 'assignee' + u.id"
|
||||
:show-username="false"
|
||||
:user="u"
|
||||
v-for="u in task.assignees"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="icon" v-if="task.attachments.length > 0">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect fill="none" rx="0" ry="0"></rect>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19.86 8.29994C19.8823 8.27664 19.9026 8.25201 19.9207 8.22634C20.5666 7.53541 20.93 6.63567 20.93 5.68001C20.93 4.69001 20.55 3.76001 19.85 3.06001C18.45 1.66001 16.02 1.66001 14.62 3.06001L9.88002 7.80001C9.86705 7.81355 9.85481 7.82753 9.8433 7.8419L4.58 13.1C3.6 14.09 3.06 15.39 3.06 16.78C3.06 18.17 3.6 19.48 4.58 20.46C5.6 21.47 6.93 21.98 8.26 21.98C9.59 21.98 10.92 21.47 11.94 20.46L17.74 14.66C17.97 14.42 17.98 14.04 17.74 13.81C17.5 13.58 17.12 13.58 16.89 13.81L11.09 19.61C10.33 20.36 9.33 20.78 8.26 20.78C7.19 20.78 6.19 20.37 5.43 19.61C4.68 18.85 4.26 17.85 4.26 16.78C4.26 15.72 4.68 14.71 5.43 13.96L15.47 3.91996C15.4962 3.89262 15.5195 3.86346 15.54 3.83292C16.4992 2.95103 18.0927 2.98269 19.01 3.90001C19.48 4.37001 19.74 5.00001 19.74 5.67001C19.74 6.34001 19.48 6.97001 19.01 7.44001L14.27 12.18C14.2571 12.1935 14.2448 12.2075 14.2334 12.2218L8.96 17.4899C8.59 17.8699 7.93 17.8699 7.55 17.4899C7.36 17.2999 7.26 17.0399 7.26 16.7799C7.26 16.5199 7.36 16.2699 7.55 16.0699L15.47 8.14994C15.7 7.90994 15.71 7.52994 15.47 7.29994C15.23 7.06994 14.85 7.06994 14.62 7.29994L6.7 15.2199C6.29 15.6399 6.06 16.1899 6.06 16.7799C6.06 17.3699 6.29 17.9199 6.7 18.3399C7.12 18.7499 7.67 18.9799 8.26 18.9799C8.85 18.9799 9.4 18.7599 9.82 18.3399L19.86 8.29994Z"></path>
|
||||
clip-rule="evenodd"
|
||||
d="M19.86 8.29994C19.8823 8.27664 19.9026 8.25201 19.9207 8.22634C20.5666 7.53541 20.93 6.63567 20.93 5.68001C20.93 4.69001 20.55 3.76001 19.85 3.06001C18.45 1.66001 16.02 1.66001 14.62 3.06001L9.88002 7.80001C9.86705 7.81355 9.85481 7.82753 9.8433 7.8419L4.58 13.1C3.6 14.09 3.06 15.39 3.06 16.78C3.06 18.17 3.6 19.48 4.58 20.46C5.6 21.47 6.93 21.98 8.26 21.98C9.59 21.98 10.92 21.47 11.94 20.46L17.74 14.66C17.97 14.42 17.98 14.04 17.74 13.81C17.5 13.58 17.12 13.58 16.89 13.81L11.09 19.61C10.33 20.36 9.33 20.78 8.26 20.78C7.19 20.78 6.19 20.37 5.43 19.61C4.68 18.85 4.26 17.85 4.26 16.78C4.26 15.72 4.68 14.71 5.43 13.96L15.47 3.91996C15.4962 3.89262 15.5195 3.86346 15.54 3.83292C16.4992 2.95103 18.0927 2.98269 19.01 3.90001C19.48 4.37001 19.74 5.00001 19.74 5.67001C19.74 6.34001 19.48 6.97001 19.01 7.44001L14.27 12.18C14.2571 12.1935 14.2448 12.2075 14.2334 12.2218L8.96 17.4899C8.59 17.8699 7.93 17.8699 7.55 17.4899C7.36 17.2999 7.26 17.0399 7.26 16.7799C7.26 16.5199 7.36 16.2699 7.55 16.0699L15.47 8.14994C15.7 7.90994 15.71 7.52994 15.47 7.29994C15.23 7.06994 14.85 7.06994 14.62 7.29994L6.7 15.2199C6.29 15.6399 6.06 16.1899 6.06 16.7799C6.06 17.3699 6.29 17.9199 6.7 18.3399C7.12 18.7499 7.67 18.9799 8.26 18.9799C8.85 18.9799 9.4 18.7599 9.82 18.3399L19.86 8.29994Z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
@ -156,16 +156,16 @@
|
||||
<div class="control">
|
||||
<input
|
||||
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Enter the new task text..."
|
||||
v-focus.always
|
||||
@focusout="toggleShowNewTaskInput(bucket.id)"
|
||||
@keyup.esc="toggleShowNewTaskInput(bucket.id)"
|
||||
@keyup.enter="addTaskToBucket(bucket.id)"
|
||||
v-model="newTaskText"
|
||||
:disabled="taskService.loading"
|
||||
:class="{'is-loading': taskService.loading}"
|
||||
:class="{'is-loading': taskService.loading}"
|
||||
:disabled="taskService.loading"
|
||||
@focusout="toggleShowNewTaskInput(bucket.id)"
|
||||
@keyup.enter="addTaskToBucket(bucket.id)"
|
||||
@keyup.esc="toggleShowNewTaskInput(bucket.id)"
|
||||
class="input"
|
||||
placeholder="Enter the new task text..."
|
||||
type="text"
|
||||
v-focus.always
|
||||
v-model="newTaskText"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="newTaskError[bucket.id] && newTaskText === ''">
|
||||
@ -173,9 +173,9 @@
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
class="button noshadow is-transparent is-fullwidth has-text-centered"
|
||||
@click="toggleShowNewTaskInput(bucket.id)"
|
||||
v-if="!showNewTaskInput[bucket.id]">
|
||||
@click="toggleShowNewTaskInput(bucket.id)"
|
||||
class="button noshadow is-transparent is-fullwidth has-text-centered"
|
||||
v-if="!showNewTaskInput[bucket.id]">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
@ -191,21 +191,21 @@
|
||||
|
||||
<div class="bucket new-bucket" v-if="!loading && canWrite">
|
||||
<input
|
||||
v-if="showNewBucketInput"
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Enter the new bucket title..."
|
||||
v-focus.always
|
||||
@focusout="() => showNewBucketInput = false"
|
||||
@keyup.esc="() => showNewBucketInput = false"
|
||||
@keyup.enter="createNewBucket"
|
||||
v-model="newBucketTitle"
|
||||
:disabled="loading"
|
||||
:class="{'is-loading': loading}"
|
||||
:class="{'is-loading': loading}"
|
||||
:disabled="loading"
|
||||
@focusout="() => showNewBucketInput = false"
|
||||
@keyup.enter="createNewBucket"
|
||||
@keyup.esc="() => showNewBucketInput = false"
|
||||
class="input"
|
||||
placeholder="Enter the new bucket title..."
|
||||
type="text"
|
||||
v-focus.always
|
||||
v-if="showNewBucketInput"
|
||||
v-model="newBucketTitle"
|
||||
/>
|
||||
<a
|
||||
class="button noshadow is-transparent is-fullwidth has-text-centered"
|
||||
@click="() => showNewBucketInput = true" v-if="!showNewBucketInput">
|
||||
@click="() => showNewBucketInput = true"
|
||||
class="button noshadow is-transparent is-fullwidth has-text-centered" v-if="!showNewBucketInput">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
@ -221,9 +221,9 @@
|
||||
</transition>
|
||||
|
||||
<modal
|
||||
v-if="showBucketDeleteModal"
|
||||
@close="showBucketDeleteModal = false"
|
||||
@submit="deleteBucket()">
|
||||
@close="showBucketDeleteModal = false"
|
||||
@submit="deleteBucket()"
|
||||
v-if="showBucketDeleteModal">
|
||||
<span slot="header">Delete the bucket</span>
|
||||
<p slot="text">
|
||||
Are you sure you want to delete this bucket?<br/>
|
||||
@ -235,158 +235,147 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TaskService from '../../../services/task'
|
||||
import TaskModel from '../../../models/task'
|
||||
import BucketModel from '../../../models/bucket'
|
||||
import TaskService from '../../../services/task'
|
||||
import TaskModel from '../../../models/task'
|
||||
import BucketModel from '../../../models/bucket'
|
||||
|
||||
import {Container, Draggable} from 'vue-smooth-dnd'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import User from '../../../components/misc/user'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import {Container, Draggable} from 'vue-smooth-dnd'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import User from '../../../components/misc/user'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
|
||||
import {filterObject} from '../../../helpers/filterObject'
|
||||
import {applyDrag} from '../../../helpers/applyDrag'
|
||||
import {mapState} from 'vuex'
|
||||
import {LOADING} from '../../../store/mutation-types'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import Rights from '../../../models/rights.json'
|
||||
import {filterObject} from '@/helpers/filterObject'
|
||||
import {applyDrag} from '@/helpers/applyDrag'
|
||||
import {mapState} from 'vuex'
|
||||
import {LOADING} from '@/store/mutation-types'
|
||||
import {saveListView} from '@/helpers/saveListView'
|
||||
import Rights from '../../../models/rights.json'
|
||||
|
||||
export default {
|
||||
name: 'Kanban',
|
||||
components: {
|
||||
Container,
|
||||
Draggable,
|
||||
Labels,
|
||||
User,
|
||||
PriorityLabel,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
taskService: TaskService,
|
||||
export default {
|
||||
name: 'Kanban',
|
||||
components: {
|
||||
Container,
|
||||
Draggable,
|
||||
Labels,
|
||||
User,
|
||||
PriorityLabel,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
taskService: TaskService,
|
||||
|
||||
dropPlaceholderOptions: {
|
||||
className: 'drop-preview',
|
||||
animationDuration: 150,
|
||||
showOnTop: true,
|
||||
},
|
||||
sourceBucket: 0,
|
||||
bucketOptionsDropDownActive: {},
|
||||
dropPlaceholderOptions: {
|
||||
className: 'drop-preview',
|
||||
animationDuration: 150,
|
||||
showOnTop: true,
|
||||
},
|
||||
sourceBucket: 0,
|
||||
bucketOptionsDropDownActive: {},
|
||||
|
||||
showBucketDeleteModal: false,
|
||||
bucketToDelete: 0,
|
||||
showBucketDeleteModal: false,
|
||||
bucketToDelete: 0,
|
||||
|
||||
newTaskText: '',
|
||||
showNewTaskInput: {},
|
||||
newBucketTitle: '',
|
||||
showNewBucketInput: false,
|
||||
newTaskError: {},
|
||||
showSetLimitInput: false,
|
||||
newTaskText: '',
|
||||
showNewTaskInput: {},
|
||||
newBucketTitle: '',
|
||||
showNewBucketInput: false,
|
||||
newTaskError: {},
|
||||
showSetLimitInput: false,
|
||||
|
||||
// We're using this to show the loading animation only at the task when updating it
|
||||
taskUpdating: {},
|
||||
// We're using this to show the loading animation only at the task when updating it
|
||||
taskUpdating: {},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
this.loadBuckets()
|
||||
this.$nextTick(() => document.addEventListener('click', this.closeBucketDropdowns))
|
||||
|
||||
// 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)
|
||||
},
|
||||
watch: {
|
||||
'$route.params.listId': 'loadBuckets',
|
||||
},
|
||||
computed: mapState({
|
||||
buckets: state => state.kanban.buckets,
|
||||
loadedListId: state => state.kanban.listId,
|
||||
loading: LOADING,
|
||||
canWrite: state => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
loadBuckets() {
|
||||
|
||||
// Prevent trying to load buckets if the task popup view is active
|
||||
if (this.$route.name !== 'list.kanban') {
|
||||
return
|
||||
}
|
||||
|
||||
// Only load buckets if we don't already loaded them
|
||||
if (
|
||||
this.loadedListId === this.$route.params.listId ||
|
||||
this.loadedListId === parseInt(this.$route.params.listId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params)
|
||||
|
||||
this.$store.dispatch('kanban/loadBucketsForList', this.$route.params.listId)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
this.loadBuckets()
|
||||
this.$nextTick(() => document.addEventListener('click', this.closeBucketDropdowns))
|
||||
onDrop(bucketId, dropResult) {
|
||||
|
||||
// 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)
|
||||
},
|
||||
watch: {
|
||||
'$route.params.listId': 'loadBuckets',
|
||||
},
|
||||
computed: mapState({
|
||||
buckets: state => state.kanban.buckets,
|
||||
loadedListId: state => state.kanban.listId,
|
||||
loading: LOADING,
|
||||
canWrite: state => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
loadBuckets() {
|
||||
// Note: A lot of this example comes from the excellent kanban example on https://github.com/kutlugsahin/vue-smooth-dnd/blob/master/demo/src/pages/cards.vue
|
||||
|
||||
// Prevent trying to load buckets if the task popup view is active
|
||||
if (this.$route.name !== 'list.kanban') {
|
||||
return
|
||||
const bucketIndex = filterObject(this.buckets, b => b.id === bucketId)
|
||||
|
||||
if (dropResult.removedIndex !== null || dropResult.addedIndex !== null) {
|
||||
|
||||
// FIXME: This is probably not the best solution and more of a naive brute-force approach
|
||||
|
||||
// Duplicate the buckets to avoid stuff moving around without noticing
|
||||
const buckets = Object.assign({}, this.buckets)
|
||||
// Get the index of the bucket and the bucket itself
|
||||
const bucket = buckets[bucketIndex]
|
||||
|
||||
// Rebuild the tasks from the bucket, removing/adding the moved task
|
||||
bucket.tasks = applyDrag(bucket.tasks, dropResult)
|
||||
// Update the bucket in the list of all buckets
|
||||
delete buckets[bucketIndex]
|
||||
buckets[bucketIndex] = bucket
|
||||
// Set the buckets, triggering a state update in vue
|
||||
// FIXME: This seems to set some task attributes (like due date) wrong. Commented out, but seems to still work?
|
||||
// Not sure what to do about this.
|
||||
// this.$store.commit('kanban/setBuckets', buckets)
|
||||
}
|
||||
|
||||
if (dropResult.addedIndex !== null) {
|
||||
|
||||
const taskIndex = dropResult.addedIndex
|
||||
const taskBefore = typeof this.buckets[bucketIndex].tasks[taskIndex - 1] === 'undefined' ? null : this.buckets[bucketIndex].tasks[taskIndex - 1]
|
||||
const taskAfter = typeof this.buckets[bucketIndex].tasks[taskIndex + 1] === 'undefined' ? null : this.buckets[bucketIndex].tasks[taskIndex + 1]
|
||||
const task = this.buckets[bucketIndex].tasks[taskIndex]
|
||||
this.$set(this.taskUpdating, task.id, true)
|
||||
|
||||
// If there is no task before, our task is the first task in which case we let it have half of the position of the task after it
|
||||
if (taskBefore === null && taskAfter !== null) {
|
||||
task.position = taskAfter.position / 2
|
||||
}
|
||||
// If there is no task after it, we just add 2^16 to the last position
|
||||
if (taskBefore !== null && taskAfter === null) {
|
||||
task.position = taskBefore.position + Math.pow(2, 16)
|
||||
}
|
||||
// If we have both a task before and after it, we acually calculate the position
|
||||
if (taskAfter !== null && taskBefore !== null) {
|
||||
task.position = taskBefore.position + (taskAfter.position - taskBefore.position) / 2
|
||||
}
|
||||
|
||||
// Only load buckets if we don't already loaded them
|
||||
if (
|
||||
this.loadedListId === this.$route.params.listId ||
|
||||
this.loadedListId === parseInt(this.$route.params.listId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
task.bucketId = bucketId
|
||||
|
||||
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params)
|
||||
|
||||
this.$store.dispatch('kanban/loadBucketsForList', this.$route.params.listId)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
onDrop(bucketId, dropResult) {
|
||||
|
||||
// Note: A lot of this example comes from the excellent kanban example on https://github.com/kutlugsahin/vue-smooth-dnd/blob/master/demo/src/pages/cards.vue
|
||||
|
||||
const bucketIndex = filterObject(this.buckets, b => b.id === bucketId)
|
||||
|
||||
if (dropResult.removedIndex !== null || dropResult.addedIndex !== null) {
|
||||
|
||||
// FIXME: This is probably not the best solution and more of a naive brute-force approach
|
||||
|
||||
// Duplicate the buckets to avoid stuff moving around without noticing
|
||||
const buckets = Object.assign({}, this.buckets)
|
||||
// Get the index of the bucket and the bucket itself
|
||||
const bucket = buckets[bucketIndex]
|
||||
|
||||
// Rebuild the tasks from the bucket, removing/adding the moved task
|
||||
bucket.tasks = applyDrag(bucket.tasks, dropResult)
|
||||
// Update the bucket in the list of all buckets
|
||||
delete buckets[bucketIndex]
|
||||
buckets[bucketIndex] = bucket
|
||||
// Set the buckets, triggering a state update in vue
|
||||
// FIXME: This seems to set some task attributes (like due date) wrong. Commented out, but seems to still work?
|
||||
// Not sure what to do about this.
|
||||
// this.$store.commit('kanban/setBuckets', buckets)
|
||||
}
|
||||
|
||||
if (dropResult.addedIndex !== null) {
|
||||
|
||||
const taskIndex = dropResult.addedIndex
|
||||
const taskBefore = typeof this.buckets[bucketIndex].tasks[taskIndex - 1] === 'undefined' ? null : this.buckets[bucketIndex].tasks[taskIndex - 1]
|
||||
const taskAfter = typeof this.buckets[bucketIndex].tasks[taskIndex + 1] === 'undefined' ? null : this.buckets[bucketIndex].tasks[taskIndex + 1]
|
||||
const task = this.buckets[bucketIndex].tasks[taskIndex]
|
||||
this.$set(this.taskUpdating, task.id, true)
|
||||
|
||||
// If there is no task before, our task is the first task in which case we let it have half of the position of the task after it
|
||||
if (taskBefore === null && taskAfter !== null) {
|
||||
task.position = taskAfter.position / 2
|
||||
}
|
||||
// If there is no task after it, we just add 2^16 to the last position
|
||||
if (taskBefore !== null && taskAfter === null) {
|
||||
task.position = taskBefore.position + Math.pow(2, 16)
|
||||
}
|
||||
// If we have both a task before and after it, we acually calculate the position
|
||||
if (taskAfter !== null && taskBefore !== null) {
|
||||
task.position = taskBefore.position + (taskAfter.position - taskBefore.position) / 2
|
||||
}
|
||||
|
||||
task.bucketId = bucketId
|
||||
|
||||
this.$store.dispatch('tasks/update', task)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this.taskUpdating, task.id, false)
|
||||
})
|
||||
}
|
||||
},
|
||||
markTaskAsDone(task) {
|
||||
task.done = !task.done
|
||||
this.$store.dispatch('tasks/update', task)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
@ -394,144 +383,155 @@
|
||||
.finally(() => {
|
||||
this.$set(this.taskUpdating, task.id, false)
|
||||
})
|
||||
},
|
||||
getTaskPayload(bucketId) {
|
||||
return index => {
|
||||
const bucket = this.buckets[filterObject(this.buckets, b => b.id === bucketId)]
|
||||
this.sourceBucket = bucket.id
|
||||
return bucket.tasks[index]
|
||||
}
|
||||
},
|
||||
toggleShowNewTaskInput(bucket) {
|
||||
this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[bucket])
|
||||
},
|
||||
toggleBucketDropdown(bucketId) {
|
||||
this.closeBucketDropdowns() // Close all eventually open dropdowns
|
||||
this.$set(this.bucketOptionsDropDownActive, bucketId, !this.bucketOptionsDropDownActive[bucketId])
|
||||
},
|
||||
closeBucketDropdowns() {
|
||||
this.showSetLimitInput = false
|
||||
for (const bucketId in this.bucketOptionsDropDownActive) {
|
||||
this.bucketOptionsDropDownActive[bucketId] = false
|
||||
}
|
||||
},
|
||||
addTaskToBucket(bucketId) {
|
||||
}
|
||||
},
|
||||
markTaskAsDone(task) {
|
||||
task.done = !task.done
|
||||
this.$store.dispatch('tasks/update', task)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this.taskUpdating, task.id, false)
|
||||
})
|
||||
},
|
||||
getTaskPayload(bucketId) {
|
||||
return index => {
|
||||
const bucket = this.buckets[filterObject(this.buckets, b => b.id === bucketId)]
|
||||
this.sourceBucket = bucket.id
|
||||
return bucket.tasks[index]
|
||||
}
|
||||
},
|
||||
toggleShowNewTaskInput(bucket) {
|
||||
this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[bucket])
|
||||
},
|
||||
toggleBucketDropdown(bucketId) {
|
||||
this.closeBucketDropdowns() // Close all eventually open dropdowns
|
||||
this.$set(this.bucketOptionsDropDownActive, bucketId, !this.bucketOptionsDropDownActive[bucketId])
|
||||
},
|
||||
closeBucketDropdowns() {
|
||||
this.showSetLimitInput = false
|
||||
for (const bucketId in this.bucketOptionsDropDownActive) {
|
||||
this.bucketOptionsDropDownActive[bucketId] = false
|
||||
}
|
||||
},
|
||||
addTaskToBucket(bucketId) {
|
||||
|
||||
if (this.newTaskText === '') {
|
||||
this.$set(this.newTaskError, bucketId, true)
|
||||
return
|
||||
}
|
||||
this.$set(this.newTaskError, bucketId, false)
|
||||
if (this.newTaskText === '') {
|
||||
this.$set(this.newTaskError, bucketId, true)
|
||||
return
|
||||
}
|
||||
this.$set(this.newTaskError, bucketId, false)
|
||||
|
||||
// We need the actual bucket index so we put that in a seperate function
|
||||
const bucketIndex = () => {
|
||||
for (const t in this.buckets) {
|
||||
if (this.buckets[t].id === bucketId) {
|
||||
return t
|
||||
}
|
||||
// We need the actual bucket index so we put that in a seperate function
|
||||
const bucketIndex = () => {
|
||||
for (const t in this.buckets) {
|
||||
if (this.buckets[t].id === bucketId) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bi = bucketIndex()
|
||||
const bi = bucketIndex()
|
||||
|
||||
const task = new TaskModel({
|
||||
title: this.newTaskText,
|
||||
bucketId: this.buckets[bi].id,
|
||||
listId: this.$route.params.listId,
|
||||
const task = new TaskModel({
|
||||
title: this.newTaskText,
|
||||
bucketId: this.buckets[bi].id,
|
||||
listId: this.$route.params.listId,
|
||||
})
|
||||
|
||||
this.taskService.create(task)
|
||||
.then(r => {
|
||||
this.newTaskText = ''
|
||||
this.$store.commit('kanban/addTaskToBucket', r)
|
||||
})
|
||||
|
||||
this.taskService.create(task)
|
||||
.then(r => {
|
||||
this.newTaskText = ''
|
||||
this.$store.commit('kanban/addTaskToBucket', r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!this.$refs[`tasks-container${task.bucketId}`][0]) {
|
||||
return
|
||||
}
|
||||
this.$refs[`tasks-container${task.bucketId}`][0].scrollTop = this.$refs[`tasks-container${task.bucketId}`][0].scrollHeight
|
||||
})
|
||||
},
|
||||
createNewBucket() {
|
||||
if (this.newBucketTitle === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const newBucket = new BucketModel({
|
||||
title: this.newBucketTitle,
|
||||
listId: parseInt(this.$route.params.listId),
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
|
||||
this.$store.dispatch('kanban/createBucket', newBucket)
|
||||
.then(() => {
|
||||
this.newBucketTitle = ''
|
||||
this.showNewBucketInput = false
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteBucketModal(bucketId) {
|
||||
if (this.buckets.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
this.bucketToDelete = bucketId
|
||||
this.showBucketDeleteModal = true
|
||||
},
|
||||
deleteBucket() {
|
||||
const bucket = new BucketModel({
|
||||
id: this.bucketToDelete,
|
||||
listId: this.$route.params.listId,
|
||||
.finally(() => {
|
||||
if (!this.$refs[`tasks-container${task.bucketId}`][0]) {
|
||||
return
|
||||
}
|
||||
this.$refs[`tasks-container${task.bucketId}`][0].scrollTop = this.$refs[`tasks-container${task.bucketId}`][0].scrollHeight
|
||||
})
|
||||
|
||||
this.$store.dispatch('kanban/deleteBucket', bucket)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.showBucketDeleteModal = false
|
||||
})
|
||||
},
|
||||
saveBucketTitle(bucketId) {
|
||||
const bucketTitle = this.$refs[`bucket${bucketId}title`][0].textContent
|
||||
const bucket = new BucketModel({
|
||||
id: bucketId,
|
||||
title: bucketTitle,
|
||||
listId: Number(this.$route.params.listId),
|
||||
})
|
||||
|
||||
// Because the contenteditable does not have a change event,
|
||||
// we're building it ourselves here and only updating the bucket
|
||||
// if the title changed.
|
||||
const realBucket = this.buckets[filterObject(this.buckets, b => b.id === bucketId)]
|
||||
if (realBucket.title === bucketTitle) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('kanban/updateBucket', bucket)
|
||||
.then(r => {
|
||||
realBucket.title = r.title
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
updateBucket(bucket) {
|
||||
bucket.limit = parseInt(bucket.limit)
|
||||
this.$store.dispatch('kanban/updateBucket', bucket)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
shouldAcceptDrop(bucket) {
|
||||
return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible
|
||||
bucket.limit === 0 || // If there is no limit set, dragging & dropping should always work
|
||||
bucket.tasks.length < bucket.limit // Disallow dropping to buckets which have their limit reached
|
||||
},
|
||||
},
|
||||
}
|
||||
createNewBucket() {
|
||||
if (this.newBucketTitle === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const newBucket = new BucketModel({
|
||||
title: this.newBucketTitle,
|
||||
listId: parseInt(this.$route.params.listId),
|
||||
})
|
||||
|
||||
this.$store.dispatch('kanban/createBucket', newBucket)
|
||||
.then(() => {
|
||||
this.newBucketTitle = ''
|
||||
this.showNewBucketInput = false
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteBucketModal(bucketId) {
|
||||
if (this.buckets.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
this.bucketToDelete = bucketId
|
||||
this.showBucketDeleteModal = true
|
||||
},
|
||||
deleteBucket() {
|
||||
const bucket = new BucketModel({
|
||||
id: this.bucketToDelete,
|
||||
listId: this.$route.params.listId,
|
||||
})
|
||||
|
||||
this.$store.dispatch('kanban/deleteBucket', bucket)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.showBucketDeleteModal = false
|
||||
})
|
||||
},
|
||||
saveBucketTitle(bucketId) {
|
||||
const bucketTitle = this.$refs[`bucket${bucketId}title`][0].textContent
|
||||
const bucket = new BucketModel({
|
||||
id: bucketId,
|
||||
title: bucketTitle,
|
||||
listId: Number(this.$route.params.listId),
|
||||
})
|
||||
|
||||
// Because the contenteditable does not have a change event,
|
||||
// we're building it ourselves here and only updating the bucket
|
||||
// if the title changed.
|
||||
const realBucket = this.buckets[filterObject(this.buckets, b => b.id === bucketId)]
|
||||
if (realBucket.title === bucketTitle) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('kanban/updateBucket', bucket)
|
||||
.then(r => {
|
||||
realBucket.title = r.title
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
updateBucket(bucket) {
|
||||
bucket.limit = parseInt(bucket.limit)
|
||||
this.$store.dispatch('kanban/updateBucket', bucket)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
shouldAcceptDrop(bucket) {
|
||||
return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible
|
||||
bucket.limit === 0 || // If there is no limit set, dragging & dropping should always work
|
||||
bucket.tasks.length < bucket.limit // Disallow dropping to buckets which have their limit reached
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<div class="loader-container is-max-width-desktop" :class="{ 'is-loading': taskCollectionService.loading}">
|
||||
<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="field has-addons" :class="{ 'hidden': !showTaskSearch }">
|
||||
<div :class="{ 'hidden': !showTaskSearch }" class="field has-addons">
|
||||
<div class="control has-icons-left has-icons-right">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
v-focus
|
||||
v-model="searchTerm"
|
||||
@keyup.enter="searchTasks"
|
||||
@blur="hideSearchBar()"/>
|
||||
@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="button noshadow is-primary"
|
||||
@click="searchTasks"
|
||||
:class="{'is-loading': taskCollectionService.loading}">
|
||||
:class="{'is-loading': taskCollectionService.loading}"
|
||||
@click="searchTasks"
|
||||
class="button noshadow is-primary">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="button" @click="showTaskSearch = !showTaskSearch" v-if="!showTaskSearch">
|
||||
<button @click="showTaskSearch = !showTaskSearch" class="button" v-if="!showTaskSearch">
|
||||
<span class="icon">
|
||||
<icon icon="search"/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="button" @click="showTaskFilter = !showTaskFilter">
|
||||
<button @click="showTaskFilter = !showTaskFilter" class="button">
|
||||
<span class="icon is-small">
|
||||
<icon icon="filter"/>
|
||||
</span>
|
||||
@ -41,30 +41,30 @@
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<filters
|
||||
v-if="showTaskFilter"
|
||||
v-model="params"
|
||||
@change="loadTasks(1)"
|
||||
@change="loadTasks(1)"
|
||||
v-if="showTaskFilter"
|
||||
v-model="params"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="field task-add" v-if="!list.isArchived && canWrite">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
|
||||
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
|
||||
<input
|
||||
v-focus
|
||||
class="input"
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
v-model="newTaskText"
|
||||
type="text"
|
||||
placeholder="Add a new task..."
|
||||
@keyup.enter="addTask()"/>
|
||||
: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 class="button is-success" :disabled="newTaskText.length === 0" @click="addTask()">
|
||||
<button :disabled="newTaskText.length === 0" @click="addTask()" class="button is-success">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
@ -77,24 +77,24 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p v-if="tasks.length === 0" class="list-is-empty-notice">
|
||||
<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="tasks" v-if="tasks && tasks.length > 0" :class="{'short': isTaskEdit}">
|
||||
<div :class="{'short': isTaskEdit}" class="tasks" v-if="tasks && tasks.length > 0">
|
||||
<single-task-in-list
|
||||
v-for="t in tasks"
|
||||
:disabled="!canWrite"
|
||||
:key="t.id"
|
||||
:the-task="t"
|
||||
@taskUpdated="updateTasks"
|
||||
task-detail-route="task.detail"
|
||||
:disabled="!canWrite"
|
||||
v-for="t in tasks"
|
||||
>
|
||||
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
|
||||
<icon icon="pencil-alt"/>
|
||||
</div>
|
||||
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
|
||||
<icon icon="pencil-alt"/>
|
||||
</div>
|
||||
</single-task-in-list>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,7 +104,7 @@
|
||||
<p class="card-header-title">
|
||||
Edit Task
|
||||
</p>
|
||||
<a class="card-header-icon" @click="isTaskEdit = false">
|
||||
<a @click="isTaskEdit = false" class="card-header-icon">
|
||||
<span class="icon">
|
||||
<icon icon="angle-right"/>
|
||||
</span>
|
||||
@ -120,22 +120,22 @@
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="pagination is-centered"
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
v-if="taskCollectionService.totalPages > 1">
|
||||
aria-label="pagination"
|
||||
class="pagination is-centered"
|
||||
role="navigation"
|
||||
v-if="taskCollectionService.totalPages > 1">
|
||||
<router-link
|
||||
class="pagination-previous"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
tag="button"
|
||||
:disabled="currentPage === 1">
|
||||
:disabled="currentPage === 1"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
class="pagination-previous"
|
||||
tag="button">
|
||||
Previous
|
||||
</router-link>
|
||||
<router-link
|
||||
class="pagination-next"
|
||||
:to="getRouteForPagination(currentPage + 1)"
|
||||
tag="button"
|
||||
:disabled="currentPage === taskCollectionService.totalPages">
|
||||
:disabled="currentPage === taskCollectionService.totalPages"
|
||||
:to="getRouteForPagination(currentPage + 1)"
|
||||
class="pagination-next"
|
||||
tag="button">
|
||||
Next page
|
||||
</router-link>
|
||||
<ul class="pagination-list">
|
||||
@ -143,10 +143,10 @@
|
||||
<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)"
|
||||
:class="{'is-current': p.number === currentPage}"
|
||||
class="pagination-link"
|
||||
:aria-label="'Goto page ' + p.number">
|
||||
:aria-label="'Goto page ' + p.number"
|
||||
:class="{'is-current': p.number === currentPage}"
|
||||
:to="getRouteForPagination(p.number)"
|
||||
class="pagination-link">
|
||||
{{ p.number }}
|
||||
</router-link>
|
||||
</li>
|
||||
@ -163,201 +163,201 @@
|
||||
</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 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'
|
||||
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,
|
||||
list: {},
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
newTaskText: '',
|
||||
export default {
|
||||
name: 'List',
|
||||
data() {
|
||||
return {
|
||||
taskService: TaskService,
|
||||
list: {},
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
newTaskText: '',
|
||||
|
||||
showError: false,
|
||||
labelTaskService: LabelTaskService,
|
||||
labelService: LabelService,
|
||||
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,
|
||||
}),
|
||||
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
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
taskList,
|
||||
],
|
||||
components: {
|
||||
Filters,
|
||||
SingleTaskInList,
|
||||
EditTask,
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
this.labelService = new LabelService()
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
this.showError = false
|
||||
|
||||
// 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,
|
||||
}),
|
||||
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 = ''
|
||||
|
||||
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) {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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})
|
||||
}))
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
// 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])
|
||||
|
||||
// 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,
|
||||
// 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)
|
||||
})
|
||||
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,
|
||||
})
|
||||
.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)
|
||||
})
|
||||
this.labelTaskService.create(labelTask)
|
||||
.then(result => {
|
||||
task.labels.push(res)
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
})
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
|
||||
// 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]
|
||||
}
|
||||
})
|
||||
.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
|
||||
}
|
||||
}
|
||||
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()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sortTasks()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div class="table-view loader-container" :class="{'is-loading': taskCollectionService.loading}">
|
||||
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
|
||||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<button class="button" @click="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}">
|
||||
<button @click="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}"
|
||||
class="button">
|
||||
<span class="icon is-small">
|
||||
<icon icon="th"/>
|
||||
</span>
|
||||
Columns
|
||||
</button>
|
||||
<button class="button" @click="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}">
|
||||
<button @click="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}"
|
||||
class="button">
|
||||
<span class="icon is-small">
|
||||
<icon icon="filter"/>
|
||||
</span>
|
||||
@ -21,22 +23,29 @@
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">Done</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">Title</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">Priority</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.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>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">Created By
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<filters
|
||||
v-if="showTaskFilter"
|
||||
v-model="params"
|
||||
@change="loadTasks(1)"
|
||||
@change="loadTasks(1)"
|
||||
v-if="showTaskFilter"
|
||||
v-model="params"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
@ -96,7 +105,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in tasks" :key="t.id">
|
||||
<tr :key="t.id" v-for="t in tasks">
|
||||
<td v-if="activeColumns.id">
|
||||
<router-link :to="{name: 'task.detail', params: { id: t.id }}">
|
||||
<template v-if="t.identifier === ''">
|
||||
@ -121,12 +130,12 @@
|
||||
</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"
|
||||
:avatar-size="27"
|
||||
:is-inline="true"
|
||||
:key="t.id + 'assignee' + a.id + i"
|
||||
:show-username="false"
|
||||
:user="a"
|
||||
v-for="(a, i) in t.assignees"
|
||||
/>
|
||||
</td>
|
||||
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
|
||||
@ -137,22 +146,43 @@
|
||||
<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"/>
|
||||
:avatar-size="27"
|
||||
:show-username="false"
|
||||
:user="t.createdBy"/>
|
||||
</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>
|
||||
<nav
|
||||
aria-label="pagination"
|
||||
class="pagination is-centered"
|
||||
role="navigation"
|
||||
v-if="taskCollectionService.totalPages > 1">
|
||||
<router-link
|
||||
:disabled="currentPage === 1"
|
||||
:to="getRouteForPagination(currentPage - 1, 'table')"
|
||||
class="pagination-previous"
|
||||
tag="button">
|
||||
Previous
|
||||
</router-link>
|
||||
<router-link
|
||||
:disabled="currentPage === taskCollectionService.totalPages"
|
||||
:to="getRouteForPagination(currentPage + 1, 'table')"
|
||||
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 :to="getRouteForPagination(p.number, 'table')" :class="{'is-current': p.number === currentPage}" class="pagination-link" :aria-label="'Goto page ' + p.number">{{ p.number }}</router-link>
|
||||
<router-link
|
||||
:aria-label="'Goto page ' + p.number"
|
||||
:class="{'is-current': p.number === currentPage}"
|
||||
:to="getRouteForPagination(p.number, 'table')"
|
||||
class="pagination-link">{{ p.number }}
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
@ -167,100 +197,100 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import taskList from '../../../components/tasks/mixins/taskList'
|
||||
import User from '../../../components/misc/user'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import DateTableCell from '../../../components/tasks/partials/date-table-cell'
|
||||
import Fancycheckbox from '../../../components/input/fancycheckbox'
|
||||
import Sort from '../../../components/tasks/partials/sort'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import Filters from '../../../components/list/partials/filters'
|
||||
import taskList from '../../../components/tasks/mixins/taskList'
|
||||
import User from '../../../components/misc/user'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import DateTableCell from '../../../components/tasks/partials/date-table-cell'
|
||||
import Fancycheckbox from '../../../components/input/fancycheckbox'
|
||||
import Sort from '../../../components/tasks/partials/sort'
|
||||
import {saveListView} from '@/helpers/saveListView'
|
||||
import Filters from '../../../components/list/partials/filters'
|
||||
|
||||
export default {
|
||||
name: 'Table',
|
||||
components: {
|
||||
Filters,
|
||||
Sort,
|
||||
Fancycheckbox,
|
||||
DateTableCell,
|
||||
Labels,
|
||||
PriorityLabel,
|
||||
User,
|
||||
},
|
||||
mixins: [
|
||||
taskList,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
showActiveColumnsFilter: false,
|
||||
activeColumns: {
|
||||
id: true,
|
||||
done: true,
|
||||
title: true,
|
||||
priority: false,
|
||||
labels: true,
|
||||
assignees: true,
|
||||
dueDate: true,
|
||||
startDate: false,
|
||||
endDate: false,
|
||||
percentDone: false,
|
||||
created: false,
|
||||
updated: false,
|
||||
createdBy: false,
|
||||
},
|
||||
sortBy: {
|
||||
id: 'desc',
|
||||
},
|
||||
}
|
||||
},
|
||||
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.$set(this.params, 'filter_by', [])
|
||||
this.$set(this.params, 'filter_value', [])
|
||||
this.$set(this.params, 'filter_comparator', [])
|
||||
|
||||
this.initTasks(1)
|
||||
|
||||
// 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)
|
||||
},
|
||||
methods: {
|
||||
initTasks(page, search = '') {
|
||||
const params = this.params
|
||||
params.sort_by = []
|
||||
params.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)
|
||||
export default {
|
||||
name: 'Table',
|
||||
components: {
|
||||
Filters,
|
||||
Sort,
|
||||
Fancycheckbox,
|
||||
DateTableCell,
|
||||
Labels,
|
||||
PriorityLabel,
|
||||
User,
|
||||
},
|
||||
mixins: [
|
||||
taskList,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
showActiveColumnsFilter: false,
|
||||
activeColumns: {
|
||||
id: true,
|
||||
done: true,
|
||||
title: true,
|
||||
priority: false,
|
||||
labels: true,
|
||||
assignees: true,
|
||||
dueDate: true,
|
||||
startDate: false,
|
||||
endDate: false,
|
||||
percentDone: false,
|
||||
created: false,
|
||||
updated: false,
|
||||
createdBy: false,
|
||||
},
|
||||
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))
|
||||
sortBy: {
|
||||
id: 'desc',
|
||||
},
|
||||
}
|
||||
},
|
||||
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.$set(this.params, 'filter_by', [])
|
||||
this.$set(this.params, 'filter_value', [])
|
||||
this.$set(this.params, 'filter_comparator', [])
|
||||
|
||||
this.initTasks(1)
|
||||
|
||||
// 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)
|
||||
},
|
||||
methods: {
|
||||
initTasks(page, search = '') {
|
||||
const params = this.params
|
||||
params.sort_by = []
|
||||
params.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>
|
||||
|
Reference in New Issue
Block a user