Merge pull request 'feature/vue3-async-await' (#832) from dpschen/frontend:feature/vue3-async-await into vue3

Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/832
This commit is contained in:
dpschen 2021-10-17 15:18:09 +00:00
commit 4e893a3196
87 changed files with 1629 additions and 2247 deletions

View file

@ -29,6 +29,7 @@
"highlight.js": "11.2.0", "highlight.js": "11.2.0",
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"marked": "3.0.7", "marked": "3.0.7",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"snake-case": "3.0.4", "snake-case": "3.0.4",

View file

@ -54,6 +54,7 @@ export default defineComponent({
this.setupAccountDeletionVerification() this.setupAccountDeletionVerification()
}, },
beforeCreate() { beforeCreate() {
// FIXME: async action in beforeCreate, might be not finished when component mounts
this.$store.dispatch('config/update') this.$store.dispatch('config/update')
.then(() => { .then(() => {
this.$store.dispatch('auth/checkAuth') this.$store.dispatch('auth/checkAuth')
@ -88,29 +89,30 @@ export default defineComponent({
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine)) window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine))
}, },
setupPasswortResetRedirect() { setupPasswortResetRedirect() {
if (typeof this.$route.query.userPasswordReset !== 'undefined') { if (typeof this.$route.query.userPasswordReset === 'undefined') {
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token return
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
this.$router.push({name: 'user.password-reset.reset'})
} }
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
this.$router.push({name: 'user.password-reset.reset'})
}, },
setupEmailVerificationRedirect() { setupEmailVerificationRedirect() {
if (typeof this.$route.query.userEmailConfirm !== 'undefined') { if (typeof this.$route.query.userEmailConfirm === 'undefined') {
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token return
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
this.$router.push({name: 'user.login'})
} }
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
this.$router.push({name: 'user.login'})
}, },
setupAccountDeletionVerification() { async setupAccountDeletionVerification() {
if (typeof this.$route.query.accountDeletionConfirm !== 'undefined') { if (typeof this.$route.query.accountDeletionConfirm === 'undefined') {
const accountDeletionService = new AccountDeleteService() return
accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
.then(() => {
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
this.$store.dispatch('auth/refreshUserInfo')
})
.catch(e => this.$message.error(e))
} }
const accountDeletionService = new AccountDeleteService()
await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
this.$store.dispatch('auth/refreshUserInfo')
}, },
}, },
}) })

View file

@ -128,9 +128,6 @@ export default {
}, },
loadLabels() { loadLabels() {
this.$store.dispatch('labels/loadAllLabels') this.$store.dispatch('labels/loadAllLabels')
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -54,14 +54,14 @@
<span <span
@click="toggleLists(n.id)" @click="toggleLists(n.id)"
class="menu-label" class="menu-label"
v-tooltip="getNamespaceTitle(n) + ' (' + n.lists.filter(l => !l.isArchived).length + ')'"> v-tooltip="namespaceTitles[nk]">
<span class="name"> <span class="name">
<span <span
:style="{ backgroundColor: n.hexColor }" :style="{ backgroundColor: n.hexColor }"
class="color-bubble" class="color-bubble"
v-if="n.hexColor !== ''"> v-if="n.hexColor !== ''">
</span> </span>
{{ getNamespaceTitle(n) }} ({{ n.lists.filter(l => !l.isArchived).length }}) {{ namespaceTitles[nk] }}
</span> </span>
</span> </span>
<a <a
@ -117,7 +117,7 @@
@click="navigate" @click="navigate"
:href="href" :href="href"
class="list-menu-link" class="list-menu-link"
:class="{'router-link-exact-active': isActive || currentList.id === l.id}" :class="{'router-link-exact-active': isActive || currentList?.id === l.id}"
> >
<span class="icon handle"> <span class="icon handle">
<icon icon="grip-lines"/> <icon icon="grip-lines"/>
@ -191,10 +191,17 @@ export default {
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces', loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}), }),
activeLists() { activeLists() {
return this.namespaces.map(({lists}) => lists.filter(item => !item.isArchived)) return this.namespaces.map(({lists}) => lists?.filter(item => !item.isArchived))
},
namespaceTitles() {
return this.namespaces.map((namespace, index) => {
const title = this.getNamespaceTitle(namespace)
return `${title} (${this.activeLists[index]?.length ?? 0})`
})
}, },
}, },
beforeCreate() { beforeCreate() {
// FIXME: async action in beforeCreate, might be unfinished when component mounts
this.$store.dispatch('namespaces/loadNamespaces') this.$store.dispatch('namespaces/loadNamespaces')
.then(namespaces => { .then(namespaces => {
namespaces.forEach(n => { namespaces.forEach(n => {
@ -218,18 +225,13 @@ export default {
return return
} }
this.$store.dispatch('lists/toggleListFavorite', list) this.$store.dispatch('lists/toggleListFavorite', list)
.catch(e => this.$message.error(e))
}, },
resize() { resize() {
// Hide the menu by default on mobile // Hide the menu by default on mobile
if (window.innerWidth < 770) { this.$store.commit(MENU_ACTIVE, window.innerWidth >= 770)
this.$store.commit(MENU_ACTIVE, false)
} else {
this.$store.commit(MENU_ACTIVE, true)
}
}, },
toggleLists(namespaceId) { toggleLists(namespaceId) {
this.listsVisible[namespaceId] = !this.listsVisible[namespaceId] ?? false this.listsVisible[namespaceId] = !this.listsVisible[namespaceId]
}, },
updateActiveLists(namespace, activeLists) { updateActiveLists(namespace, activeLists) {
// this is a bit hacky: since we do have to filter out the archived items from the list // this is a bit hacky: since we do have to filter out the archived items from the list
@ -249,7 +251,8 @@ export default {
this.$store.commit('namespaces/setNamespaceById', newNamespace) this.$store.commit('namespaces/setNamespaceById', newNamespace)
}, },
saveListPosition(e, namespaceIndex) {
async saveListPosition(e, namespaceIndex) {
const listsActive = this.activeLists[namespaceIndex] const listsActive = this.activeLists[namespaceIndex]
const list = listsActive[e.newIndex] const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null const listBefore = listsActive[e.newIndex - 1] ?? null
@ -258,17 +261,15 @@ export default {
const position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null) const position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null)
// create a copy of the list in order to not violate vuex mutations try {
this.$store.dispatch('lists/updateList', { // create a copy of the list in order to not violate vuex mutations
...list, await this.$store.dispatch('lists/updateList', {
position, ...list,
}) position,
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.listUpdating[list.id] = false
}) })
} finally {
this.listUpdating[list.id] = false
}
}, },
}, },
} }

View file

@ -233,7 +233,7 @@ export default {
// dom tree. If we're calling this right after setting this.preview it could be the images were // dom tree. If we're calling this right after setting this.preview it could be the images were
// not already made available. // not already made available.
// Some docs at https://stackoverflow.com/q/62865160/10924593 // Some docs at https://stackoverflow.com/q/62865160/10924593
this.$nextTick(() => { this.$nextTick(async () => {
const attachmentImage = document.getElementsByClassName('attachment-image') const attachmentImage = document.getElementsByClassName('attachment-image')
if (attachmentImage) { if (attachmentImage) {
for (const img of attachmentImage) { for (const img of attachmentImage) {
@ -254,11 +254,9 @@ export default {
this.attachmentService = new AttachmentService() this.attachmentService = new AttachmentService()
} }
this.attachmentService.getBlobUrl(attachment) const url = await this.attachmentService.getBlobUrl(attachment)
.then(url => { img.src = url
img.src = url this.loadedAttachments[cacheKey] = url
this.loadedAttachments[cacheKey] = url
})
} }
} }

View file

@ -468,7 +468,7 @@ export default {
this.filters.done = true this.filters.done = true
} }
}, },
prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) { async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
if (filterName === null) { if (filterName === null) {
filterName = kind filterName = kind
} }
@ -478,13 +478,11 @@ export default {
} }
this.prepareSingleValue(filterName) this.prepareSingleValue(filterName)
if (typeof this.filters[filterName] !== 'undefined' && this.filters[filterName] !== '') { if (typeof this.filters[filterName] === 'undefined' || this.filters[filterName] === '') {
this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]}) return
.then(r => {
this[kind] = r
})
.catch(e => this.$message.error(e))
} }
this[kind] = await this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]})
}, },
setDoneFilter() { setDoneFilter() {
if (this.filters.done) { if (this.filters.done) {
@ -524,20 +522,16 @@ export default {
clear(kind) { clear(kind) {
this[`found${kind}`] = [] this[`found${kind}`] = []
}, },
find(kind, query) { async find(kind, query) {
if (query === '') { if (query === '') {
this.clear(kind) this.clear(kind)
} }
this[`${kind}Service`].getAll({}, {s: query}) const response = await this[`${kind}Service`].getAll({}, {s: query})
.then(response => {
// Filter users from the results who are already assigned // Filter users from the results who are already assigned
this[`found${kind}`] = response.filter(({id}) => !includesById(this[kind], id)) this[`found${kind}`] = response.filter(({id}) => !includesById(this[kind], id))
})
.catch(e => {
this.$message.error(e)
})
}, },
add(kind, filterName) { add(kind, filterName) {
this.$nextTick(() => { this.$nextTick(() => {

View file

@ -56,7 +56,7 @@ export default {
}, },
}, },
methods: { methods: {
loadBackground() { async loadBackground() {
if (this.list === null || !this.list.backgroundInformation || this.backgroundLoading) { if (this.list === null || !this.list.backgroundInformation || this.backgroundLoading) {
return return
} }
@ -64,14 +64,11 @@ export default {
this.backgroundLoading = true this.backgroundLoading = true
const listService = new ListService() const listService = new ListService()
listService.background(this.list) try {
.then(b => { this.background = await listService.background(this.list)
this.background = b } finally {
}) this.backgroundLoading = false
.catch(e => { }
this.$message.error(e)
})
.finally(() => this.backgroundLoading = false)
}, },
toggleFavoriteList(list) { toggleFavoriteList(list) {
// The favorites pseudo list is always favorite // The favorites pseudo list is always favorite
@ -80,7 +77,6 @@ export default {
return return
} }
this.$store.dispatch('lists/toggleListFavorite', list) this.$store.dispatch('lists/toggleListFavorite', list)
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -138,58 +138,35 @@ export default {
} }
this.migrate() this.migrate()
}) })
.catch(e => {
this.$message.error(e)
})
} }
}, },
methods: { methods: {
getAuthUrl() { async getAuthUrl() {
this.migrationService.getAuthUrl() const { url } = await this.migrationService.getAuthUrl()
.then(r => { this.authUrl = url
this.authUrl = r.url
})
.catch(e => {
this.$message.error(e)
})
}, },
migrate() {
async migrate() {
this.isMigrating = true this.isMigrating = true
this.lastMigrationDate = null this.lastMigrationDate = null
this.message = '' this.message = ''
let migrationConfig = { code: this.migratorAuthCode }
if (this.isFileMigrator) { if (this.isFileMigrator) {
return this.migrateFile() if (this.$refs.uploadInput.files.length === 0) {
return
}
migrationConfig = this.$refs.uploadInput.files[0]
} }
this.migrationService.migrate({code: this.migratorAuthCode}) try {
.then(r => { const { message } = await this.migrationService.migrate(migrationConfig)
this.message = r.message this.message = message
this.$store.dispatch('namespaces/loadNamespaces') return this.$store.dispatch('namespaces/loadNamespaces')
}) } finally {
.catch(e => { this.isMigrating = false
this.$message.error(e)
})
.finally(() => {
this.isMigrating = false
})
},
migrateFile() {
if (this.$refs.uploadInput.files.length === 0) {
return
} }
this.migrationService.migrate(this.$refs.uploadInput.files[0])
.then(r => {
this.message = r.message
this.$store.dispatch('namespaces/loadNamespaces')
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.isMigrating = false
})
}, },
}, },
} }

View file

@ -106,7 +106,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch((e) => { .catch((e) => {
// Check if it has a port and if not check if it is reachable at https // Check if it has a port and if not check if it is reachable at https
@ -115,7 +115,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch((e) => { .catch((e) => {
// Check if it is reachable at /api/v1 and https // Check if it is reachable at /api/v1 and https
@ -128,7 +128,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch((e) => { .catch((e) => {
// Check if it is reachable at port API_DEFAULT_PORT and https // Check if it is reachable at port API_DEFAULT_PORT and https
@ -138,7 +138,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch((e) => { .catch((e) => {
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https // Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
@ -151,7 +151,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch((e) => { .catch((e) => {
// Check if it is reachable at port API_DEFAULT_PORT and http // Check if it is reachable at port API_DEFAULT_PORT and http
@ -161,7 +161,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch((e) => { .catch((e) => {
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http // Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
@ -174,7 +174,7 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) throw e
}) })
.catch(() => { .catch(() => {
// Still not found, url is still invalid // Still not found, url is still invalid

View file

@ -89,33 +89,23 @@ export default {
this.unsubscribe() this.unsubscribe()
} }
}, },
subscribe() { async subscribe() {
const subscription = new SubscriptionModel({ const subscription = new SubscriptionModel({
entity: this.entity, entity: this.entity,
entityId: this.entityId, entityId: this.entityId,
}) })
this.subscriptionService.create(subscription) await this.subscriptionService.create(subscription)
.then(() => { this.$emit('change', subscription)
this.$emit('change', subscription) this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})})
this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})})
})
.catch(e => {
this.$message.error(e)
})
}, },
unsubscribe() { async unsubscribe() {
const subscription = new SubscriptionModel({ const subscription = new SubscriptionModel({
entity: this.entity, entity: this.entity,
entityId: this.entityId, entityId: this.entityId,
}) })
this.subscriptionService.delete(subscription) await this.subscriptionService.delete(subscription)
.then(() => { this.$emit('change', null)
this.$emit('change', null) this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})})
this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})})
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -93,14 +93,8 @@ export default {
closeWhenClickedOutside(e, this.$refs.popup, () => this.showNotifications = false) closeWhenClickedOutside(e, this.$refs.popup, () => this.showNotifications = false)
} }
}, },
loadNotifications() { async loadNotifications() {
this.notificationService.getAll() this.allNotifications = await this.notificationService.getAll()
.then(r => {
this.allNotifications = r
})
.catch(e => {
this.$message.error(e)
})
}, },
to(n, index) { to(n, index) {
const to = { const to = {
@ -127,17 +121,13 @@ export default {
break break
} }
return () => { return async () => {
if (to.name !== '') { if (to.name !== '') {
this.$router.push(to) this.$router.push(to)
} }
n.read = true n.read = true
this.notificationService.update(n) this.allNotifications[index] = await this.notificationService.update(n)
.then(r => {
this.allNotifications[index] = r
})
.catch(e => this.$message.error(e))
} }
}, },
}, },

View file

@ -282,20 +282,17 @@ export default {
this.taskSearchTimeout = null this.taskSearchTimeout = null
} }
this.taskSearchTimeout = setTimeout(() => { this.taskSearchTimeout = setTimeout(async () => {
this.taskService.getAll({}, {s: query}) const r = await this.taskService.getAll({}, {s: query})
.then(r => { this.foundTasks = r.map(t => {
r = r.map(t => { t.type = TYPE_TASK
t.type = TYPE_TASK const list = this.$store.getters['lists/getListById'](t.listId)
const list = this.$store.getters['lists/getListById'](t.listId) === null ? null : this.$store.getters['lists/getListById'](t.listId) if (list !== null) {
if (list !== null) { t.title = `${t.title} (${list.title})`
t.title = `${t.title} (${list.title})` }
}
return t return t
}) })
this.foundTasks = r
})
}, 150) }, 150)
}, },
searchTeams() { searchTeams() {
@ -318,15 +315,12 @@ export default {
this.teamSearchTimeout = null this.teamSearchTimeout = null
} }
this.teamSearchTimeout = setTimeout(() => { this.teamSearchTimeout = setTimeout(async () => {
this.teamService.getAll({}, {s: query}) const r = await this.teamService.getAll({}, {s: query})
.then(r => { this.foundTeams = r.map(t => {
r = r.map(t => { t.title = t.name
t.title = t.name return t
return t })
})
this.foundTeams = r
})
}, 150) }, 150)
}, },
closeQuickActions() { closeQuickActions() {
@ -378,25 +372,20 @@ export default {
break break
} }
}, },
newTask() { async newTask() {
if (this.currentList === null) { if (this.currentList === null) {
return return
} }
this.$store.dispatch('tasks/createNewTask', { const task = await this.$store.dispatch('tasks/createNewTask', {
title: this.query, title: this.query,
listId: this.currentList.id, listId: this.currentList.id,
}) })
.then(r => { this.$message.success({message: this.$t('task.createSuccess')})
this.$message.success({message: this.$t('task.createSuccess')}) this.$router.push({name: 'task.detail', params: {id: task.id}})
this.$router.push({name: 'task.detail', params: {id: r.id}}) this.closeQuickActions()
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
}, },
newList() { async newList() {
if (this.currentList === null) { if (this.currentList === null) {
return return
} }
@ -405,42 +394,27 @@ export default {
title: this.query, title: this.query,
namespaceId: this.currentList.namespaceId, namespaceId: this.currentList.namespaceId,
}) })
this.$store.dispatch('lists/createList', newList) const list = await this.$store.dispatch('lists/createList', newList)
.then(r => { this.$message.success({message: this.$t('list.create.createdSuccess')})
this.$message.success({message: this.$t('list.create.createdSuccess')}) this.$router.push({name: 'list.index', params: {listId: list.id}})
this.$router.push({name: 'list.index', params: {listId: r.id}}) this.closeQuickActions()
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
}, },
newNamespace() { async newNamespace() {
const newNamespace = new NamespaceModel({title: this.query}) const newNamespace = new NamespaceModel({title: this.query})
this.$store.dispatch('namespaces/createNamespace', newNamespace) await this.$store.dispatch('namespaces/createNamespace', newNamespace)
.then(() => { this.$message.success({message: this.$t('namespace.create.success')})
this.$message.success({message: this.$t('namespace.create.success')}) this.closeQuickActions()
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
}, },
newTeam() { async newTeam() {
const newTeam = new TeamModel({name: this.query}) const newTeam = new TeamModel({name: this.query})
this.teamService.create(newTeam) const team = await this.teamService.create(newTeam)
.then(r => { this.$router.push({
this.$router.push({ name: 'teams.edit',
name: 'teams.edit', params: {id: team.id},
params: {id: r.id}, })
}) this.$message.success({message: this.$t('team.create.success')})
this.$message.success({message: this.$t('team.create.success')}) this.closeQuickActions()
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
}, },
select(parentIndex, index) { select(parentIndex, index) {

View file

@ -215,59 +215,41 @@ export default {
frontendUrl: (state) => state.config.frontendUrl, frontendUrl: (state) => state.config.frontendUrl,
}), }),
methods: { methods: {
load(listId) { async load(listId) {
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here // If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
if (listId === 0) { if (listId === 0) {
return return
} }
this.linkShareService this.linkShares = await this.linkShareService.getAll({listId})
.getAll({listId})
.then((r) => {
this.linkShares = r
})
.catch((e) => {
this.$message.error(e)
})
}, },
add(listId) { async add(listId) {
const newLinkShare = new LinkShareModel({ const newLinkShare = new LinkShareModel({
right: this.selectedRight, right: this.selectedRight,
listId, listId,
name: this.name, name: this.name,
password: this.password, password: this.password,
}) })
this.linkShareService await this.linkShareService.create(newLinkShare)
.create(newLinkShare) this.selectedRight = rights.READ
.then(() => { this.name = ''
this.selectedRight = rights.READ this.password = ''
this.name = '' this.showNewForm = false
this.password = '' this.$message.success({message: this.$t('list.share.links.createSuccess')})
this.showNewForm = false await this.load(listId)
this.$message.success({message: this.$t('list.share.links.createSuccess')})
this.load(listId)
})
.catch((e) => {
this.$message.error(e)
})
}, },
remove(listId) { async remove(listId) {
const linkshare = new LinkShareModel({ const linkshare = new LinkShareModel({
id: this.linkIdToDelete, id: this.linkIdToDelete,
listId, listId,
}) })
this.linkShareService try {
.delete(linkshare) await this.linkShareService.delete(linkshare)
.then(() => { this.$message.success({message: this.$t('list.share.links.deleteSuccess')})
this.$message.success({message: this.$t('list.share.links.deleteSuccess')}) await this.load(listId)
this.load(listId) } finally {
}) this.showDeleteModal = false
.catch((e) => { }
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
})
}, },
copy, copy,
getShareLink(hash) { getShareLink(hash) {

View file

@ -272,45 +272,34 @@ export default {
this.load() this.load()
}, },
methods: { methods: {
load() { async load() {
this.stuffService this.sharables = await this.stuffService.getAll(this.stuffModel)
.getAll(this.stuffModel) this.sharables.forEach((s) =>
.then((r) => { this.selectedRight[s.id] = s.right,
this.sharables = r )
r.forEach((s) =>
this.selectedRight[s.id] = s.right,
)
})
.catch((e) => {
this.$message.error(e)
})
}, },
deleteSharable() {
async deleteSharable() {
if (this.shareType === 'user') { if (this.shareType === 'user') {
this.stuffModel.userId = this.sharable.username this.stuffModel.userId = this.sharable.username
} else if (this.shareType === 'team') { } else if (this.shareType === 'team') {
this.stuffModel.teamId = this.sharable.id this.stuffModel.teamId = this.sharable.id
} }
this.stuffService await this.stuffService.delete(this.stuffModel)
.delete(this.stuffModel) this.showDeleteModal = false
.then(() => { for (const i in this.sharables) {
this.showDeleteModal = false if (
for (const i in this.sharables) { (this.sharables[i].username === this.stuffModel.userId && this.shareType === 'user') ||
if ( (this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team')
(this.sharables[i].username === this.stuffModel.userId && this.shareType === 'user') || ) {
(this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team') this.sharables.splice(i, 1)
) { }
this.sharables.splice(i, 1) }
} this.$message.success({message: this.$t('list.share.userTeam.removeSuccess', {type: this.shareTypeName, sharable: this.sharableName})})
}
this.$message.success({message: this.$t('list.share.userTeam.removeSuccess', {type: this.shareTypeName, sharable: this.sharableName})})
})
.catch((e) => {
this.$message.error(e)
})
}, },
add(admin) {
async add(admin) {
if (admin === null) { if (admin === null) {
admin = false admin = false
} }
@ -325,17 +314,12 @@ export default {
this.stuffModel.teamId = this.sharable.id this.stuffModel.teamId = this.sharable.id
} }
this.stuffService await this.stuffService.create(this.stuffModel)
.create(this.stuffModel) this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})})
.then(() => { await this.load()
this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})})
this.load()
})
.catch((e) => {
this.$message.error(e)
})
}, },
toggleType(sharable) {
async toggleType(sharable) {
if ( if (
this.selectedRight[sharable.id] !== rights.ADMIN && this.selectedRight[sharable.id] !== rights.ADMIN &&
this.selectedRight[sharable.id] !== rights.READ && this.selectedRight[sharable.id] !== rights.READ &&
@ -351,41 +335,30 @@ export default {
this.stuffModel.teamId = sharable.id this.stuffModel.teamId = sharable.id
} }
this.stuffService const r = await this.stuffService.update(this.stuffModel)
.update(this.stuffModel) for (const i in this.sharables) {
.then((r) => { if (
for (const i in this.sharables) { (this.sharables[i].username ===
if ( this.stuffModel.userId &&
(this.sharables[i].username === this.shareType === 'user') ||
this.stuffModel.userId && (this.sharables[i].id === this.stuffModel.teamId &&
this.shareType === 'user') || this.shareType === 'team')
(this.sharables[i].id === this.stuffModel.teamId && ) {
this.shareType === 'team') this.sharables[i].right = r.right
) { }
this.sharables[i].right = r.right }
} this.$message.success({message: this.$t('list.share.userTeam.updatedSuccess', {type: this.shareTypeName})})
}
this.$message.success({message: this.$t('list.share.userTeam.updatedSuccess', {type: this.shareTypeName})})
})
.catch((e) => {
this.$message.error(e)
})
}, },
find(query) {
async find(query) {
if (query === '') { if (query === '') {
this.clearAll() this.clearAll()
return return
} }
this.searchService this.found = await this.searchService.getAll({}, {s: query})
.getAll({}, {s: query})
.then((response) => {
this.found = response
})
.catch((e) => {
this.$message.error(e)
})
}, },
clearAll() { clearAll() {
this.found = [] this.found = []
}, },

View file

@ -82,7 +82,7 @@ export default {
this.initialTextAreaHeight = this.$refs.newTaskInput.scrollHeight + INPUT_BORDER_PX this.initialTextAreaHeight = this.$refs.newTaskInput.scrollHeight + INPUT_BORDER_PX
}, },
methods: { methods: {
addTask() { async addTask() {
if (this.newTaskTitle === '') { if (this.newTaskTitle === '') {
this.errorMessage = this.$t('list.create.addTitleRequired') this.errorMessage = this.$t('list.create.addTitleRequired')
return return
@ -93,37 +93,31 @@ export default {
return return
} }
const newTasks = [] const newTasks = this.newTaskTitle.split(/[\r\n]+/).map(async t => {
this.newTaskTitle.split(/[\r\n]+/).forEach(t => {
const title = cleanupTitle(t) const title = cleanupTitle(t)
if (title === '') { if (title === '') {
return return
} }
newTasks.push( const task = await this.$store.dispatch('tasks/createNewTask', {
this.$store.dispatch('tasks/createNewTask', { title: this.newTaskTitle,
title: this.newTaskTitle, listId: this.$store.state.auth.settings.defaultListId,
listId: this.$store.state.auth.settings.defaultListId, position: this.defaultPosition,
position: this.defaultPosition, })
}) this.$emit('taskAdded', task)
.then(task => { return task
this.$emit('taskAdded', task)
return task
}),
)
}) })
Promise.all(newTasks) try {
.then(() => { await Promise.all(newTasks)
this.newTaskTitle = '' this.newTaskTitle = ''
}) } catch(e) {
.catch(e => { if (e.message === 'NO_LIST') {
if (e === 'NO_LIST') { this.errorMessage = this.$t('list.create.addListRequired')
this.errorMessage = this.$t('list.create.addListRequired') return
return }
} throw e
this.$message.error(e) }
})
}, },
handleEnter(e) { handleEnter(e) {
// when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create // when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create

View file

@ -134,17 +134,10 @@ export default {
this.editorActive = false this.editorActive = false
this.$nextTick(() => (this.editorActive = true)) this.$nextTick(() => (this.editorActive = true))
}, },
editTaskSubmit() { async editTaskSubmit() {
this.taskService this.taskEditTask = await this.taskService.update(this.taskEditTask)
.update(this.taskEditTask) this.initTaskFields()
.then((r) => { this.$message.success({message: this.$t('task.detail.updateSuccess')})
this.taskEditTask = r
this.initTaskFields()
this.$message.success({message: this.$t('task.detail.updateSuccess')})
})
.catch((e) => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -297,54 +297,41 @@ export default {
console.debug('prepareGanttDays; years:', years) console.debug('prepareGanttDays; years:', years)
this.days = years this.days = years
}, },
parseTasks() { parseTasks() {
this.setDates() this.setDates()
this.loadTasks() this.loadTasks()
}, },
loadTasks() {
async loadTasks() {
this.theTasks = [] this.theTasks = []
this.tasksWithoutDates = [] this.tasksWithoutDates = []
const getAllTasks = (page = 1) => { const getAllTasks = async (page = 1) => {
return this.taskCollectionService const tasks = await this.taskCollectionService.getAll({listId: this.listId}, this.params, page)
.getAll({listId: this.listId}, this.params, page) if (page < this.taskCollectionService.totalPages) {
.then((tasks) => { const nextTasks = await getAllTasks(page + 1)
if (page < this.taskCollectionService.totalPages) { return tasks.concat(nextTasks)
return getAllTasks(page + 1).then((nextTasks) => { }
return tasks.concat(nextTasks) return tasks
})
} else {
return tasks
}
})
.catch((e) => {
return Promise.reject(e)
})
} }
getAllTasks() const tasks = await getAllTasks()
.then((tasks) => { this.theTasks = tasks
this.theTasks = tasks .filter((t) => {
.filter((t) => { if (t.startDate === null && !t.done) {
if (t.startDate === null && !t.done) { this.tasksWithoutDates.push(t)
this.tasksWithoutDates.push(t) }
} return (
return ( t.startDate >= this.startDate &&
t.startDate >= this.startDate && t.endDate <= this.endDate
t.endDate <= this.endDate )
)
})
.map((t) => {
return this.addGantAttributes(t)
})
.sort(function (a, b) {
if (a.startDate < b.startDate) return -1
if (a.startDate > b.startDate) return 1
return 0
})
}) })
.catch((e) => { .map((t) => this.addGantAttributes(t))
this.$message.error(e) .sort(function (a, b) {
if (a.startDate < b.startDate) return -1
if (a.startDate > b.startDate) return 1
return 0
}) })
}, },
addGantAttributes(t) { addGantAttributes(t) {
@ -357,7 +344,7 @@ export default {
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24) t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24)
return t return t
}, },
resizeTask(taskDragged, newRect) { async resizeTask(taskDragged, newRect) {
if (this.isTaskEdit) { if (this.isTaskEdit) {
return return
} }
@ -398,34 +385,28 @@ export default {
offsetDays: newTask.offsetDays, offsetDays: newTask.offsetDays,
} }
this.taskService const r = await this.taskService.update(newTask)
.update(newTask) r.endDate = ganttData.endDate
.then(r => { r.durationDays = ganttData.durationDays
r.endDate = ganttData.endDate r.offsetDays = ganttData.offsetDays
r.durationDays = ganttData.durationDays
r.offsetDays = ganttData.offsetDays
// If the task didn't have dates before, we'll update the list // If the task didn't have dates before, we'll update the list
if (didntHaveDates) { if (didntHaveDates) {
for (const t in this.tasksWithoutDates) { for (const t in this.tasksWithoutDates) {
if (this.tasksWithoutDates[t].id === r.id) { if (this.tasksWithoutDates[t].id === r.id) {
this.tasksWithoutDates.splice(t, 1) this.tasksWithoutDates.splice(t, 1)
break break
}
}
this.theTasks.push(this.addGantAttributes(r))
} else {
for (const tt in this.theTasks) {
if (this.theTasks[tt].id === r.id) {
this.theTasks[tt] = this.addGantAttributes(r)
break
}
}
} }
}) }
.catch((e) => { this.theTasks.push(this.addGantAttributes(r))
this.$message.error(e) } else {
}) for (const tt in this.theTasks) {
if (this.theTasks[tt].id === r.id) {
this.theTasks[tt] = this.addGantAttributes(r)
break
}
}
}
}, },
editTask(task) { editTask(task) {
this.taskToEdit = task this.taskToEdit = task
@ -445,7 +426,7 @@ export default {
this.$nextTick(() => (this.newTaskFieldActive = false)) this.$nextTick(() => (this.newTaskFieldActive = false))
} }
}, },
addNewTask() { async addNewTask() {
if (!this.newTaskFieldActive) { if (!this.newTaskFieldActive) {
return return
} }
@ -453,16 +434,10 @@ export default {
title: this.newTaskTitle, title: this.newTaskTitle,
listId: this.listId, listId: this.listId,
}) })
this.taskService const r = await this.taskService.create(task)
.create(task) this.tasksWithoutDates.push(this.addGantAttributes(r))
.then((r) => { this.newTaskTitle = ''
this.tasksWithoutDates.push(this.addGantAttributes(r)) this.hideCrateNewTask()
this.newTaskTitle = ''
this.hideCrateNewTask()
})
.catch((e) => {
this.$message.error(e)
})
}, },
formatYear(date) { formatYear(date) {
return this.format(date, 'MMMM, yyyy') return this.format(date, 'MMMM, yyyy')

View file

@ -38,7 +38,7 @@ export default {
'$route.path': 'loadTasksOnSavedFilter', '$route.path': 'loadTasksOnSavedFilter',
}, },
methods: { methods: {
loadTasks( async loadTasks(
page, page,
search = '', search = '',
params = null, params = null,
@ -76,17 +76,9 @@ export default {
} }
this.tasks = [] this.tasks = []
this.tasks = await this.taskCollectionService.getAll(list, params, page)
this.taskCollectionService.getAll(list, params, page) this.currentPage = page
.then(r => { this.loadedList = JSON.parse(JSON.stringify(currentList))
this.tasks = r
this.currentPage = page
this.loadedList = JSON.parse(JSON.stringify(currentList))
})
.catch(e => {
this.$message.error(e)
})
}, },
loadTasksForPage(e) { loadTasksForPage(e) {

View file

@ -218,24 +218,19 @@ export default {
uploadFiles(files) { uploadFiles(files) {
uploadFiles(this.attachmentService, this.taskId, files) uploadFiles(this.attachmentService, this.taskId, files)
}, },
deleteAttachment() { async deleteAttachment() {
this.attachmentService try {
.delete(this.attachmentToDelete) const r = await this.attachmentService.delete(this.attachmentToDelete)
.then((r) => { this.$store.commit(
this.$store.commit( 'attachments/removeById',
'attachments/removeById', this.attachmentToDelete.id,
this.attachmentToDelete.id, )
) this.$message.success(r)
this.$message.success(r) } finally{
}) this.showDeleteModal = false
.catch((e) => { }
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
})
}, },
viewOrDownload(attachment) { async viewOrDownload(attachment) {
if ( if (
attachment.file.name.endsWith('.jpg') || attachment.file.name.endsWith('.jpg') ||
attachment.file.name.endsWith('.png') || attachment.file.name.endsWith('.png') ||
@ -243,9 +238,7 @@ export default {
attachment.file.name.endsWith('.gif') attachment.file.name.endsWith('.gif')
) { ) {
this.showImageModal = true this.showImageModal = true
this.attachmentService.getBlobUrl(attachment).then((url) => { this.attachmentImageBlobUrl = await this.attachmentService.getBlobUrl(attachment)
this.attachmentImageBlobUrl = url
})
} else { } else {
this.downloadAttachment(attachment) this.downloadAttachment(attachment)
} }

View file

@ -134,9 +134,9 @@
<transition name="modal"> <transition name="modal">
<modal <modal
@close="showDeleteModal = false"
@submit="deleteComment()"
v-if="showDeleteModal" v-if="showDeleteModal"
@close="showDeleteModal = false"
@submit="() => deleteComment(commentToDelete)"
> >
<template #header><span>{{ $t('task.comment.delete') }}</span></template> <template #header><span>{{ $t('task.comment.delete') }}</span></template>
@ -186,7 +186,6 @@ export default {
taskCommentService: new TaskCommentService(), taskCommentService: new TaskCommentService(),
newComment: new TaskCommentModel(), newComment: new TaskCommentModel(),
editorActive: true, editorActive: true,
actions: {},
saved: null, saved: null,
saving: null, saving: null,
@ -195,43 +194,46 @@ export default {
}, },
watch: { watch: {
taskId: { taskId: {
handler(taskId) { handler: 'loadComments',
if (!this.enabled) {
return
}
this.loadComments()
this.newComment.taskId = taskId
this.commentEdit.taskId = taskId
this.commentToDelete.taskId = taskId
},
immediate: true, immediate: true,
}, },
canWrite() { },
this.makeActions() computed: {
...mapState({
userAvatar: state => state.auth.info.getAvatarUrl(48),
enabled: state => state.config.taskCommentsEnabled,
}),
actions() {
if (!this.canWrite) {
return {}
}
return Object.fromEntries(this.comments.map((c) => ([
c.id,
[{
action: () => this.toggleDelete(c.id),
title: this.$t('misc.delete'),
}],
])))
}, },
}, },
computed: mapState({
userAvatar: state => state.auth.info.getAvatarUrl(48),
enabled: state => state.config.taskCommentsEnabled,
}),
methods: { methods: {
attachmentUpload(...args) { attachmentUpload(...args) {
return uploadFile(this.taskId, ...args) return uploadFile(this.taskId, ...args)
}, },
loadComments() { async loadComments(taskId) {
this.taskCommentService if (!this.enabled) {
.getAll({taskId: this.taskId}) return
.then(r => { }
this.comments = r
this.makeActions() this.newComment.taskId = taskId
}) this.commentEdit.taskId = taskId
.catch((e) => { this.commentToDelete.taskId = taskId
this.$message.error(e) this.comments = await this.taskCommentService.getAll({taskId})
})
}, },
addComment() {
async addComment() {
if (this.newComment.comment === '') { if (this.newComment.comment === '') {
return return
} }
@ -245,30 +247,27 @@ export default {
this.$nextTick(() => (this.editorActive = true)) this.$nextTick(() => (this.editorActive = true))
this.creating = true this.creating = true
this.taskCommentService try {
.create(this.newComment) const comment = await this.taskCommentService.create(this.newComment)
.then((r) => { this.comments.push(comment)
this.comments.push(r) this.newComment.comment = ''
this.newComment.comment = '' this.$message.success({message: this.$t('task.comment.addedSuccess')})
this.$message.success({message: this.$t('task.comment.addedSuccess')}) } finally {
this.makeActions() this.creating = false
}) }
.catch((e) => {
this.$message.error(e)
})
.finally(() => {
this.creating = false
})
}, },
toggleEdit(comment) { toggleEdit(comment) {
this.isCommentEdit = !this.isCommentEdit this.isCommentEdit = !this.isCommentEdit
this.commentEdit = comment this.commentEdit = comment
}, },
toggleDelete(commentId) { toggleDelete(commentId) {
this.showDeleteModal = !this.showDeleteModal this.showDeleteModal = !this.showDeleteModal
this.commentToDelete.id = commentId this.commentToDelete.id = commentId
}, },
editComment() {
async editComment() {
if (this.commentEdit.comment === '') { if (this.commentEdit.comment === '') {
return return
} }
@ -276,54 +275,30 @@ export default {
this.saving = this.commentEdit.id this.saving = this.commentEdit.id
this.commentEdit.taskId = this.taskId this.commentEdit.taskId = this.taskId
this.taskCommentService try {
.update(this.commentEdit) const comment = this.taskCommentService.update(this.commentEdit)
.then((r) => { for (const c in this.comments) {
for (const c in this.comments) { if (this.comments[c].id === this.commentEdit.id) {
if (this.comments[c].id === this.commentEdit.id) { this.comments[c] = comment
this.comments[c] = r
}
} }
this.saved = this.commentEdit.id }
setTimeout(() => { this.saved = this.commentEdit.id
this.saved = null setTimeout(() => {
}, 2000) this.saved = null
}) }, 2000)
.catch((e) => { } finally {
this.$message.error(e) this.isCommentEdit = false
}) this.saving = null
.finally(() => { }
this.isCommentEdit = false
this.saving = null
})
}, },
deleteComment() {
this.taskCommentService async deleteComment(commentToDelete) {
.delete(this.commentToDelete) try {
.then(() => { await this.taskCommentService.delete(commentToDelete)
for (const a in this.comments) { const index = this.comments.findIndex(({id}) => id === commentToDelete.id)
if (this.comments[a].id === this.commentToDelete.id) { this.comments.splice(index, 1)
this.comments.splice(a, 1) } finally {
} this.showDeleteModal = false
}
})
.catch((e) => {
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
})
},
makeActions() {
if (this.canWrite) {
this.comments.forEach((c) => {
this.actions[c.id] = [
{
action: () => this.toggleDelete(c.id),
title: this.$t('misc.delete'),
},
]
})
} }
}, },
}, },

View file

@ -112,7 +112,8 @@ export default {
this.dueDate = this.dueDate.setDate(this.dueDate.getDate() + days) this.dueDate = this.dueDate.setDate(this.dueDate.getDate() + days)
this.updateDueDate() this.updateDueDate()
}, },
updateDueDate() {
async updateDueDate() {
if (!this.dueDate) { if (!this.dueDate) {
return return
} }
@ -122,16 +123,10 @@ export default {
} }
this.task.dueDate = new Date(this.dueDate) this.task.dueDate = new Date(this.dueDate)
this.taskService const task = await this.taskService.update(this.task)
.update(this.task) this.lastValue = task.dueDate
.then((r) => { this.task = task
this.lastValue = r.dueDate this.$emit('update:modelValue', task)
this.task = r
this.$emit('update:modelValue', r)
})
.catch((e) => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -71,24 +71,19 @@ export default {
}, },
}, },
methods: { methods: {
save() { async save() {
this.saving = true this.saving = true
this.$store.dispatch('tasks/update', this.task) try {
.then(t => { this.task = await this.$store.dispatch('tasks/update', this.task)
this.task = t this.$emit('update:modelValue', this.task)
this.$emit('update:modelValue', t) this.saved = true
this.saved = true setTimeout(() => {
setTimeout(() => { this.saved = false
this.saved = false }, 2000)
}, 2000) } finally {
}) this.saving = false
.catch(e => { }
this.$message.error(e)
})
.finally(() => {
this.saving = false
})
}, },
}, },
} }

View file

@ -78,49 +78,40 @@ export default {
}, },
}, },
methods: { methods: {
addAssignee(user) { async addAssignee(user) {
this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId}) await this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId})
.then(() => { this.$emit('update:modelValue', this.assignees)
this.$emit('update:modelValue', this.assignees) this.$message.success({message: this.$t('task.assignee.assignSuccess')})
this.$message.success({message: this.$t('task.assignee.assignSuccess')})
})
.catch(e => {
this.$message.error(e)
})
}, },
removeAssignee(user) {
this.$store.dispatch('tasks/removeAssignee', {user: user, taskId: this.taskId}) async removeAssignee(user) {
.then(() => { await this.$store.dispatch('tasks/removeAssignee', {user: user, taskId: this.taskId})
// Remove the assignee from the list
for (const a in this.assignees) { // Remove the assignee from the list
if (this.assignees[a].id === user.id) { for (const a in this.assignees) {
this.assignees.splice(a, 1) if (this.assignees[a].id === user.id) {
} this.assignees.splice(a, 1)
} }
this.$message.success({message: this.$t('task.assignee.unassignSuccess')}) }
}) this.$message.success({message: this.$t('task.assignee.unassignSuccess')})
.catch(e => {
this.$message.error(e)
})
}, },
findUser(query) {
async findUser(query) {
if (query === '') { if (query === '') {
this.clearAllFoundUsers() this.clearAllFoundUsers()
return return
} }
this.listUserService.getAll({listId: this.listId}, {s: query}) const response = await this.listUserService.getAll({listId: this.listId}, {s: query})
.then(response => {
// Filter the results to not include users who are already assigned // Filter the results to not include users who are already assigned
this.foundUsers = response.filter(({id}) => !includesById(this.assignees, id)) this.foundUsers = response.filter(({id}) => !includesById(this.assignees, id))
})
.catch(e => {
this.$message.error(e)
})
}, },
clearAllFoundUsers() { clearAllFoundUsers() {
this.foundUsers = [] this.foundUsers = []
}, },
focus() { focus() {
this.$refs.multiselect.focus() this.$refs.multiselect.focus()
}, },

View file

@ -93,7 +93,8 @@ export default {
findLabel(query) { findLabel(query) {
this.query = query this.query = query
}, },
addLabel(label, showNotification = true) {
async addLabel(label, showNotification = true) {
const bubble = () => { const bubble = () => {
this.$emit('update:modelValue', this.labels) this.$emit('update:modelValue', this.labels)
this.$emit('change', this.labels) this.$emit('change', this.labels)
@ -104,18 +105,14 @@ export default {
return return
} }
this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId}) await this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
.then(() => { bubble()
bubble() if (showNotification) {
if (showNotification) { this.$message.success({message: this.$t('task.label.addSuccess')})
this.$message.success({message: this.$t('task.label.addSuccess')}) }
}
})
.catch(e => {
this.$message.error(e)
})
}, },
removeLabel(label) {
async removeLabel(label) {
const removeFromState = () => { const removeFromState = () => {
for (const l in this.labels) { for (const l in this.labels) {
if (this.labels[l].id === label.id) { if (this.labels[l].id === label.id) {
@ -131,30 +128,21 @@ export default {
return return
} }
this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId}) await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
.then(() => { removeFromState()
removeFromState() this.$message.success({message: this.$t('task.label.removeSuccess')})
this.$message.success({message: this.$t('task.label.removeSuccess')})
})
.catch(e => {
this.$message.error(e)
})
}, },
createAndAddLabel(title) {
async createAndAddLabel(title) {
if (this.taskId === 0) { if (this.taskId === 0) {
return return
} }
const newLabel = new LabelModel({title: title}) const newLabel = new LabelModel({title: title})
this.$store.dispatch('labels/createLabel', newLabel) const label = await this.$store.dispatch('labels/createLabel', newLabel)
.then(r => { this.addLabel(label, false)
this.addLabel(r, false) this.labels.push(label)
this.labels.push(r) this.$message.success({message: this.$t('task.label.addCreateSuccess')})
this.$message.success({message: this.$t('task.label.addCreateSuccess')})
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },

View file

@ -58,7 +58,7 @@ export default {
emits: ['update:modelValue'], emits: ['update:modelValue'],
methods: { methods: {
save(title) { async save(title) {
// We only want to save if the title was actually changed. // We only want to save if the title was actually changed.
// Because the contenteditable does not have a change event // Because the contenteditable does not have a change event
// we're building it ourselves and only continue // we're building it ourselves and only continue
@ -74,20 +74,17 @@ export default {
title, title,
} }
this.$store.dispatch('tasks/update', newTask) try {
.then((task) => { const task = await this.$store.dispatch('tasks/update', newTask)
this.$emit('update:modelValue', task) this.$emit('update:modelValue', task)
this.showSavedMessage = true this.showSavedMessage = true
setTimeout(() => { setTimeout(() => {
this.showSavedMessage = false this.showSavedMessage = false
}, 2000) }, 2000)
}) }
.catch(e => { finally {
this.$message.error(e) this.saving = false
}) }
.finally(() => {
this.saving = false
})
}, },
}, },
} }

View file

@ -6,9 +6,9 @@
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor, '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}" :style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
@click.ctrl="() => markTaskAsDone(task)" @click.ctrl="() => toggleTaskDone(task)"
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })" @click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
@click.meta="() => markTaskAsDone(task)" @click.meta="() => toggleTaskDone(task)"
class="task loader-container draggable" class="task loader-container draggable"
> >
<span class="task-id"> <span class="task-id">
@ -93,23 +93,19 @@ export default {
}, },
}, },
methods: { methods: {
markTaskAsDone(task) { async toggleTaskDone(task) {
this.loadingInternal = true this.loadingInternal = true
this.$store.dispatch('tasks/update', { try {
...task, await this.$store.dispatch('tasks/update', {
done: !task.done, ...task,
}) done: !task.done,
.then(() => {
if (task.done) {
playPop()
}
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.loadingInternal = false
}) })
if (task.done) {
playPop()
}
} finally {
this.loadingInternal = false
}
}, },
}, },
} }

View file

@ -50,28 +50,25 @@ export default {
}, },
}, },
methods: { methods: {
findLists(query) { async findLists(query) {
if (query === '') { if (query === '') {
this.clearAll() this.clearAll()
return return
} }
this.listSerivce.getAll({}, {s: query}) this.foundLists = await this.listSerivce.getAll({}, {s: query})
.then(response => {
this.foundLists = response
})
.catch(e => {
this.$message.error(e)
})
}, },
clearAll() { clearAll() {
this.foundLists = [] this.foundLists = []
}, },
select(list) { select(list) {
this.list = list this.list = list
this.$emit('selected', list) this.$emit('selected', list)
this.$emit('update:modelValue', list) this.$emit('update:modelValue', list)
}, },
namespace(namespaceId) { namespace(namespaceId) {
const namespace = this.$store.getters['namespaces/getNamespaceById'](namespaceId) const namespace = this.$store.getters['namespaces/getNamespaceById'](namespaceId)
if (namespace !== null) { if (namespace !== null) {

View file

@ -185,76 +185,62 @@ export default {
}, },
}, },
methods: { methods: {
findTasks(query) { async findTasks(query) {
this.taskService.getAll({}, {s: query}) this.foundTasks = await this.taskService.getAll({}, {s: query})
.then(response => {
this.foundTasks = response
})
.catch(e => {
this.$message.error(e)
})
}, },
addTaskRelation() {
let rel = new TaskRelationModel({ async addTaskRelation() {
const rel = new TaskRelationModel({
taskId: this.taskId, taskId: this.taskId,
otherTaskId: this.newTaskRelationTask.id, otherTaskId: this.newTaskRelationTask.id,
relationKind: this.newTaskRelationKind, relationKind: this.newTaskRelationKind,
}) })
this.taskRelationService.create(rel) await this.taskRelationService.create(rel)
.then(() => { if (!this.relatedTasks[this.newTaskRelationKind]) {
if (!this.relatedTasks[this.newTaskRelationKind]) { this.relatedTasks[this.newTaskRelationKind] = []
this.relatedTasks[this.newTaskRelationKind] = [] }
} this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask) this.newTaskRelationTask = null
this.newTaskRelationTask = null this.saved = true
this.saved = true this.showNewRelationForm = false
this.showNewRelationForm = false setTimeout(() => {
setTimeout(() => { this.saved = false
this.saved = false }, 2000)
}, 2000)
})
.catch(e => {
this.$message.error(e)
})
}, },
removeTaskRelation() {
async removeTaskRelation() {
const rel = new TaskRelationModel({ const rel = new TaskRelationModel({
relationKind: this.relationToDelete.relationKind, relationKind: this.relationToDelete.relationKind,
taskId: this.taskId, taskId: this.taskId,
otherTaskId: this.relationToDelete.otherTaskId, otherTaskId: this.relationToDelete.otherTaskId,
}) })
this.taskRelationService.delete(rel) try {
.then(() => { await this.taskRelationService.delete(rel)
Object.keys(this.relatedTasks).forEach(relationKind => {
for (const t in this.relatedTasks[relationKind]) { Object.entries(this.relatedTasks).some(([relationKind, t]) => {
if (this.relatedTasks[relationKind][t].id === this.relationToDelete.otherTaskId && relationKind === this.relationToDelete.relationKind) { const found = this.relatedTasks[relationKind][t].id === this.relationToDelete.otherTaskId &&
this.relatedTasks[relationKind].splice(t, 1) relationKind === this.relationToDelete.relationKind
} if (!found) return false
}
}) this.relatedTasks[relationKind].splice(t, 1)
this.saved = true return true
setTimeout(() => {
this.saved = false
}, 2000)
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
}) })
this.saved = true
setTimeout(() => {
this.saved = false
}, 2000)
} finally {
this.showDeleteModal = false
}
}, },
createAndRelateTask(title) {
async createAndRelateTask(title) {
const newTask = new TaskModel({title: title, listId: this.listId}) const newTask = new TaskModel({title: title, listId: this.listId})
this.taskService.create(newTask) this.newTaskRelationTask = await this.taskService.create(newTask)
.then(r => { await this.addTaskRelation()
this.newTaskRelationTask = r
this.addTaskRelation()
})
.catch(e => {
this.$message.error(e)
})
}, },
relationKindTitle(kind, length) { relationKindTitle(kind, length) {
return this.$tc(`task.relation.kinds.${kind}`, length) return this.$tc(`task.relation.kinds.${kind}`, length)
}, },

View file

@ -166,56 +166,47 @@ export default {
}, },
}, },
methods: { methods: {
markAsDone(checked) { async markAsDone(checked) {
const updateFunc = () => { const updateFunc = async () => {
this.taskService.update(this.task) const task = await this.taskService.update(this.task)
.then(t => { if (this.task.done) {
if (this.task.done) { playPop()
playPop() }
} this.task = task
this.task = t this.$emit('task-updated', task)
this.$emit('task-updated', t) this.$message.success({
this.$message.success({ message: this.task.done ?
message: this.task.done ? this.$t('task.doneSuccess') :
this.$t('task.doneSuccess') : this.$t('task.undoneSuccess'),
this.$t('task.undoneSuccess'), }, [{
}, [{ title: 'Undo',
title: 'Undo', callback() {
callback: () => { this.task.done = !this.task.done
this.task.done = !this.task.done this.markAsDone(!checked)
this.markAsDone(!checked) },
}, }])
}])
})
.catch(e => {
this.$message.error(e)
})
} }
if (checked) { if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else { } else {
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
} }
}, },
toggleFavorite() {
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite this.task.isFavorite = !this.task.isFavorite
this.taskService.update(this.task) this.task = await this.taskService.update(this.task)
.then(t => { this.$emit('task-updated', this.task)
this.task = t this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
this.$emit('task-updated', t)
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
})
.catch(e => {
this.$message.error(e)
})
}, },
hideDeferDueDatePopup(e) { hideDeferDueDatePopup(e) {
if (this.showDefer) { if (!this.showDefer) {
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => { return
this.showDefer = false
})
} }
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
this.showDefer = false
})
}, },
}, },
} }

View file

@ -87,43 +87,38 @@ export default {
Cropper, Cropper,
}, },
methods: { methods: {
avatarStatus() { async avatarStatus() {
this.avatarService.get({}) const { avatarProvider } = await this.avatarService.get({})
.then(r => { this.avatarProvider = avatarProvider
this.avatarProvider = r.avatarProvider
})
.catch(e => this.$message.error(e))
}, },
updateAvatarStatus() {
async updateAvatarStatus() {
const avatarStatus = new AvatarModel({avatarProvider: this.avatarProvider}) const avatarStatus = new AvatarModel({avatarProvider: this.avatarProvider})
this.avatarService.update(avatarStatus) await this.avatarService.update(avatarStatus)
.then(() => { this.$message.success({message: this.$t('user.settings.avatar.statusUpdateSuccess')})
this.$message.success({message: this.$t('user.settings.avatar.statusUpdateSuccess')}) this.$store.commit('auth/reloadAvatar')
this.$store.commit('auth/reloadAvatar')
})
.catch(e => this.$message.error(e))
}, },
uploadAvatar() {
async uploadAvatar() {
this.loading = true this.loading = true
const {canvas} = this.$refs.cropper.getResult() const {canvas} = this.$refs.cropper.getResult()
if (canvas) { if (!canvas) {
canvas.toBlob(blob => {
this.avatarService.create(blob)
.then(() => {
this.$message.success({message: this.$t('user.settings.avatar.setSuccess')})
this.$store.commit('auth/reloadAvatar')
})
.catch(e => this.$message.error(e))
.finally(() => {
this.loading = false
this.isCropAvatar = false
})
})
} else {
this.loading = false this.loading = false
return
}
try {
const blob = await new Promise(resolve => canvas.toBlob(blob => resolve(blob)))
await this.avatarService.create(blob)
this.$message.success({message: this.$t('user.settings.avatar.setSuccess')})
this.$store.commit('auth/reloadAvatar')
} finally {
this.loading = false
this.isCropAvatar = false
} }
}, },
cropAvatar() { cropAvatar() {
const avatar = this.$refs.avatarUploadInput.files const avatar = this.$refs.avatarUploadInput.files

View file

@ -43,28 +43,22 @@ export default {
name: 'data-export', name: 'data-export',
data() { data() {
return { return {
dataExportService: DataExportService, dataExportService: new DataExportService(),
password: '', password: '',
errPasswordRequired: false, errPasswordRequired: false,
} }
}, },
created() {
this.dataExportService = new DataExportService()
},
methods: { methods: {
requestDataExport() { async requestDataExport() {
if (this.password === '') { if (this.password === '') {
this.errPasswordRequired = true this.errPasswordRequired = true
this.$refs.passwordInput.focus() this.$refs.passwordInput.focus()
return return
} }
this.dataExportService.request(this.password) await this.dataExportService.request(this.password)
.then(() => { this.$message.success({message: this.$t('user.export.success')})
this.$message.success({message: this.$t('user.export.success')}) this.password = ''
this.password = ''
})
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -101,34 +101,29 @@ export default {
deletionScheduledAt: state => parseDateOrNull(state.auth.info.deletionScheduledAt), deletionScheduledAt: state => parseDateOrNull(state.auth.info.deletionScheduledAt),
}), }),
methods: { methods: {
deleteAccount() { async deleteAccount() {
if (this.password === '') { if (this.password === '') {
this.errPasswordRequired = true this.errPasswordRequired = true
this.$refs.passwordInput.focus() this.$refs.passwordInput.focus()
return return
} }
this.accountDeleteService.request(this.password) await this.accountDeleteService.request(this.password)
.then(() => { this.$message.success({message: this.$t('user.deletion.requestSuccess')})
this.$message.success({message: this.$t('user.deletion.requestSuccess')}) this.password = ''
this.password = ''
})
.catch(e => this.$message.error(e))
}, },
cancelDeletion() {
async cancelDeletion() {
if (this.password === '') { if (this.password === '') {
this.errPasswordRequired = true this.errPasswordRequired = true
this.$refs.passwordInput.focus() this.$refs.passwordInput.focus()
return return
} }
this.accountDeleteService.cancel(this.password) await this.accountDeleteService.cancel(this.password)
.then(() => { this.$message.success({message: this.$t('user.deletion.scheduledCancelSuccess')})
this.$message.success({message: this.$t('user.deletion.scheduledCancelSuccess')}) this.$store.dispatch('auth/refreshUserInfo')
this.$store.dispatch('auth/refreshUserInfo') this.password = ''
this.password = ''
})
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -11,24 +11,22 @@ export function uploadFile(taskId: number, file: FileModel, onSuccess: () => Fun
return uploadFiles(attachmentService, taskId, files, onSuccess) return uploadFiles(attachmentService, taskId, files, onSuccess)
} }
export function uploadFiles(attachmentService: AttachmentService, taskId: number, files: FileModel[], onSuccess : Function = () => {}) { export async function uploadFiles(attachmentService: AttachmentService, taskId: number, files: FileModel[], onSuccess : Function = () => {}) {
const attachmentModel = new AttachmentModel({taskId}) const attachmentModel = new AttachmentModel({taskId})
attachmentService.create(attachmentModel, files) const response = await attachmentService.create(attachmentModel, files)
.then(r => { console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
console.debug(`Uploaded attachments for task ${taskId}, response was`, r)
if (r.success !== null) { response.success?.map((attachment: AttachmentModel) => {
r.success.forEach((attachment: AttachmentModel) => { store.dispatch('tasks/addTaskAttachment', {
store.dispatch('tasks/addTaskAttachment', { taskId,
taskId, attachment,
attachment,
})
onSuccess(generateAttachmentUrl(taskId, attachment.id))
})
}
if (r.errors !== null) {
throw Error(r.errors)
}
}) })
onSuccess(generateAttachmentUrl(taskId, attachment.id))
})
if (response.errors !== null) {
throw Error(response.errors)
}
} }
export function generateAttachmentUrl(taskId: number, attachmentId: number) : any { export function generateAttachmentUrl(taskId: number, attachmentId: number) : any {

View file

@ -41,21 +41,19 @@ export const removeToken = () => {
* Refreshes an auth token while ensuring it is updated everywhere. * Refreshes an auth token while ensuring it is updated everywhere.
* @returns {Promise<AxiosResponse<any>>} * @returns {Promise<AxiosResponse<any>>}
*/ */
export const refreshToken = (persist: boolean): Promise<AxiosResponse> => { export async function refreshToken(persist: boolean): Promise<AxiosResponse> {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
return HTTP.post('user/token', null, { try {
headers: { const response = await HTTP.post('user/token', null, {
Authorization: `Bearer ${getToken()}`, headers: {
}, Authorization: `Bearer ${getToken()}`,
}) },
.then(r => {
saveToken(r.data.token, persist)
return Promise.resolve(r)
})
.catch(e => {
// eslint-disable-next-line
console.log('Error renewing token: ', e)
return Promise.reject(e)
}) })
saveToken(response.data.token, persist)
return response
} catch(e) {
throw new Error('Error renewing token: ', { cause: e })
}
} }

View file

@ -27,3 +27,14 @@ export function filterLabelsByQuery(state: labelState, labelsToHide: label[], qu
return !labelIds.includes(id) && title.toLowerCase().includes(labelQuery) return !labelIds.includes(id) && title.toLowerCase().includes(labelQuery)
}) })
} }
/**
* Returns the labels by id if found
* @param {Object} state
* @param {Array} ids
* @returns {Array}
*/
export function getLabelsByIds(state: labelState, ids: number[]) {
return Object.values(state.labels).filter(({id}) => ids.includes(id))
}

View file

@ -33,7 +33,7 @@ export const getMigratorFromSlug = (slug: string): Migrator => {
isFileMigrator: true, isFileMigrator: true,
} }
default: default:
throw Error('Unknown migrator slug ' + slug) throw new Error('Unknown migrator slug ' + slug)
} }
} }

View file

@ -27,14 +27,13 @@ const setI18nLanguage = lang => {
} }
export const loadLanguageAsync = lang => { export const loadLanguageAsync = lang => {
// If the same language if (
if (i18n.global.locale === lang) { // If the same language
return Promise.resolve(setI18nLanguage(lang)) i18n.global.locale === lang ||
} // If the language was already loaded
loadedLanguages.includes(lang)
// If the language was already loaded ) {
if (loadedLanguages.includes(lang)) { return setI18nLanguage(lang)
return Promise.resolve(setI18nLanguage(lang))
} }
// If the language hasn't been loaded yet // If the language hasn't been loaded yet

View file

@ -96,10 +96,30 @@ app.config.errorHandler = (err, vm, info) => {
// if (import.meta.env.PROD) { // if (import.meta.env.PROD) {
// error(err) // error(err)
// } else { // } else {
console.error(err, vm, info) // console.error(err, vm, info)
error(err)
// } // }
} }
if (import.meta.env.DEV) {
app.config.warnHandler = (msg, vm, info) => {
error(msg)
}
}
// https://stackoverflow.com/a/52076738/15522256
window.addEventListener('error', (err) => {
error(err)
})
window.addEventListener('unhandledrejection', (err) => {
// event.promise contains the promise object
// event.reason contains the reason for the rejection
error(err)
})
app.config.globalProperties.$message = { app.config.globalProperties.$message = {
error, error,
success, success,

View file

@ -24,16 +24,12 @@ export default class TaskModel extends AbstractModel {
this.endDate = parseDateOrNull(this.endDate) this.endDate = parseDateOrNull(this.endDate)
this.doneAt = parseDateOrNull(this.doneAt) this.doneAt = parseDateOrNull(this.doneAt)
this.reminderDates = this.reminderDates.map(d => new Date(d))
// Cancel all scheduled notifications for this task to be sure to only have available notifications // Cancel all scheduled notifications for this task to be sure to only have available notifications
this.cancelScheduledNotifications() this.cancelScheduledNotifications().then(() => {
.then(() => { // Every time we see a reminder, we schedule a notification for it
this.reminderDates = this.reminderDates.map(d => { this.reminderDates.forEach(d => this.scheduleNotification(d))
d = new Date(d) })
// Every time we see a reminder, we schedule a notification for it
this.scheduleNotification(d)
return d
})
})
// Parse the repeat after into something usable // Parse the repeat after into something usable
this.parseRepeatAfter() this.parseRepeatAfter()
@ -218,27 +214,26 @@ export default class TaskModel extends AbstractModel {
} }
// Register the actual notification // Register the actual notification
registration.showNotification('Vikunja Reminder', { try {
tag: `vikunja-task-${this.id}`, // Group notifications by task id so we're only showing one notification per task registration.showNotification('Vikunja Reminder', {
body: this.title, tag: `vikunja-task-${this.id}`, // Group notifications by task id so we're only showing one notification per task
// eslint-disable-next-line no-undef body: this.title,
showTrigger: new TimestampTrigger(date), // eslint-disable-next-line no-undef
badge: '/images/icons/badge-monochrome.png', showTrigger: new TimestampTrigger(date),
icon: '/images/icons/android-chrome-512x512.png', badge: '/images/icons/badge-monochrome.png',
data: {taskId: this.id}, icon: '/images/icons/android-chrome-512x512.png',
actions: [ data: {taskId: this.id},
{ actions: [
action: 'show-task', {
title: 'Show task', action: 'show-task',
}, title: 'Show task',
], },
}) ],
.then(() => {
console.debug('Notification scheduled for ' + date)
})
.catch(e => {
console.debug('Error scheduling notification', e)
}) })
console.debug('Notification scheduled for ' + date)
} catch(e) {
throw new Error('Error scheduling notification', e)
}
} }
} }

View file

@ -105,19 +105,6 @@ export default class AbstractService {
return true return true
} }
/////////////////////
// Global error handler
///////////////////
/**
* Handles the error and rejects the promise.
* @param error
* @returns {Promise<never>}
*/
errorHandler(error) {
return Promise.reject(error)
}
///////////////// /////////////////
// Helper functions // Helper functions
/////////////// ///////////////
@ -284,7 +271,7 @@ export default class AbstractService {
*/ */
get(model, params = {}) { get(model, params = {}) {
if (this.paths.get === '') { if (this.paths.get === '') {
return Promise.reject({message: 'This model is not able to get data.'}) throw new Error('This model is not able to get data.')
} }
return this.getM(this.paths.get, model, params) return this.getM(this.paths.get, model, params)
@ -298,35 +285,30 @@ export default class AbstractService {
* @param params * @param params
* @returns {Q.Promise<unknown>} * @returns {Q.Promise<unknown>}
*/ */
getM(url, model = {}, params = {}) { async getM(url, model = {}, params = {}) {
const cancel = this.setLoading() const cancel = this.setLoading()
model = this.beforeGet(model) model = this.beforeGet(model)
const finalUrl = this.getReplacedRoute(url, model) const finalUrl = this.getReplacedRoute(url, model)
return this.http.get(finalUrl, {params: params}) try {
.catch(error => { const response = await this.http.get(finalUrl, {params})
return this.errorHandler(error) const result = this.modelGetFactory(response.data)
}) result.maxRight = Number(response.headers['x-max-right'])
.then(response => { return result
const result = this.modelGetFactory(response.data) } finally {
result.maxRight = Number(response.headers['x-max-right']) cancel()
return Promise.resolve(result) }
})
.finally(() => {
cancel()
})
} }
getBlobUrl(url, method = 'GET', data = {}) { async getBlobUrl(url, method = 'GET', data = {}) {
return this.http({ const response = await this.http({
url: url, url: url,
method: method, method: method,
responseType: 'blob', responseType: 'blob',
data: data, data: data,
}).then(response => {
return window.URL.createObjectURL(new Blob([response.data]))
}) })
return window.URL.createObjectURL(new Blob([response.data]))
} }
/** /**
@ -337,9 +319,9 @@ export default class AbstractService {
* @param page The page to get * @param page The page to get
* @returns {Q.Promise<any>} * @returns {Q.Promise<any>}
*/ */
getAll(model = {}, params = {}, page = 1) { async getAll(model = {}, params = {}, page = 1) {
if (this.paths.getAll === '') { if (this.paths.getAll === '') {
return Promise.reject({message: 'This model is not able to get data.'}) throw new Error('This model is not able to get data.')
} }
params.page = page params.page = page
@ -348,27 +330,22 @@ export default class AbstractService {
model = this.beforeGet(model) model = this.beforeGet(model)
const finalUrl = this.getReplacedRoute(this.paths.getAll, model) const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
return this.http.get(finalUrl, {params: params}) try {
.catch(error => { const response = await this.http.get(finalUrl, {params: params})
return this.errorHandler(error) this.resultCount = Number(response.headers['x-pagination-result-count'])
}) this.totalPages = Number(response.headers['x-pagination-total-pages'])
.then(response => {
this.resultCount = Number(response.headers['x-pagination-result-count'])
this.totalPages = Number(response.headers['x-pagination-total-pages'])
if (Array.isArray(response.data)) { if (response.data === null) {
return Promise.resolve(response.data.map(entry => { return []
return this.modelGetAllFactory(entry) }
}))
} if (Array.isArray(response.data)) {
if (response.data === null) { return response.data.map(entry => this.modelGetAllFactory(entry))
return Promise.resolve([]) }
} return this.modelGetAllFactory(response.data)
return Promise.resolve(this.modelGetAllFactory(response.data)) } finally {
}) cancel()
.finally(() => { }
cancel()
})
} }
/** /**
@ -376,28 +353,24 @@ export default class AbstractService {
* @param model * @param model
* @returns {Promise<any | never>} * @returns {Promise<any | never>}
*/ */
create(model) { async create(model) {
if (this.paths.create === '') { if (this.paths.create === '') {
return Promise.reject({message: 'This model is not able to create data.'}) throw new Error('This model is not able to create data.')
} }
const cancel = this.setLoading() const cancel = this.setLoading()
const finalUrl = this.getReplacedRoute(this.paths.create, model) const finalUrl = this.getReplacedRoute(this.paths.create, model)
return this.http.put(finalUrl, model) try {
.catch(error => { const response = await this.http.put(finalUrl, model)
return this.errorHandler(error) const result = this.modelCreateFactory(response.data)
}) if (typeof model.maxRight !== 'undefined') {
.then(response => { result.maxRight = model.maxRight
const result = this.modelCreateFactory(response.data) }
if (typeof model.maxRight !== 'undefined') { return result
result.maxRight = model.maxRight } finally {
} cancel()
return Promise.resolve(result) }
})
.finally(() => {
cancel()
})
} }
/** /**
@ -407,23 +380,19 @@ export default class AbstractService {
* @param model * @param model
* @returns {Q.Promise<unknown>} * @returns {Q.Promise<unknown>}
*/ */
post(url, model) { async post(url, model) {
const cancel = this.setLoading() const cancel = this.setLoading()
return this.http.post(url, model) try {
.catch(error => { const response = await this.http.post(url, model)
return this.errorHandler(error) const result = this.modelUpdateFactory(response.data)
}) if (typeof model.maxRight !== 'undefined') {
.then(response => { result.maxRight = model.maxRight
const result = this.modelUpdateFactory(response.data) }
if (typeof model.maxRight !== 'undefined') { return result
result.maxRight = model.maxRight } finally {
} cancel()
return Promise.resolve(result) }
})
.finally(() => {
cancel()
})
} }
/** /**
@ -433,7 +402,7 @@ export default class AbstractService {
*/ */
update(model) { update(model) {
if (this.paths.update === '') { if (this.paths.update === '') {
return Promise.reject({message: 'This model is not able to update data.'}) throw new Error('This model is not able to update data.')
} }
const finalUrl = this.getReplacedRoute(this.paths.update, model) const finalUrl = this.getReplacedRoute(this.paths.update, model)
@ -445,24 +414,20 @@ export default class AbstractService {
* @param model * @param model
* @returns {Q.Promise<any>} * @returns {Q.Promise<any>}
*/ */
delete(model) { async delete(model) {
if (this.paths.delete === '') { if (this.paths.delete === '') {
return Promise.reject({message: 'This model is not able to delete data.'}) throw new Error('This model is not able to delete data.')
} }
const cancel = this.setLoading() const cancel = this.setLoading()
const finalUrl = this.getReplacedRoute(this.paths.delete, model) const finalUrl = this.getReplacedRoute(this.paths.delete, model)
return this.http.delete(finalUrl, model) try {
.catch(error => { const {data} = await this.http.delete(finalUrl, model)
return this.errorHandler(error) return data
}) } finally {
.then(response => { cancel()
return Promise.resolve(response.data) }
})
.finally(() => {
cancel()
})
} }
/** /**
@ -496,32 +461,28 @@ export default class AbstractService {
* @param formData * @param formData
* @returns {Q.Promise<unknown>} * @returns {Q.Promise<unknown>}
*/ */
uploadFormData(url, formData) { async uploadFormData(url, formData) {
console.log(formData, formData._boundary) console.log(formData, formData._boundary)
const cancel = this.setLoading() const cancel = this.setLoading()
return this.http.put( try {
url, const response = await this.http.put(
formData, url,
{ formData,
headers: { {
'Content-Type': headers: {
'Content-Type':
'multipart/form-data; boundary=' + formData._boundary, 'multipart/form-data; boundary=' + formData._boundary,
},
onUploadProgress: progressEvent => {
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
},
}, },
onUploadProgress: progressEvent => { )
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total) this.modelCreateFactory(response.data)
}, } finally {
}, this.uploadProgress = 0
) cancel()
.catch(error => { }
return this.errorHandler(error)
})
.then(response => {
return Promise.resolve(this.modelCreateFactory(response.data))
})
.finally(() => {
this.uploadProgress = 0
cancel()
})
} }
} }

View file

@ -37,9 +37,9 @@ export default class AttachmentService extends AbstractService {
return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id) return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id)
} }
download(model) { async download(model) {
this.getBlobUrl(model) const url = await this.getBlobUrl(model)
.then(url => downloadBlob(url, model.file.name)) return downloadBlob(url, model.file.name)
} }
/** /**

View file

@ -18,17 +18,12 @@ export default class BackgroundUnsplashService extends AbstractService {
return new ListModel(data) return new ListModel(data)
} }
thumb(model) { async thumb(model) {
return this.http({ const response = await this.http({
url: `/backgrounds/unsplash/images/${model.id}/thumb`, url: `/backgrounds/unsplash/images/${model.id}/thumb`,
method: 'GET', method: 'GET',
responseType: 'blob', responseType: 'blob',
}) })
.then(response => { return window.URL.createObjectURL(new Blob([response.data]))
return window.URL.createObjectURL(new Blob([response.data]))
})
.catch(e => {
return e
})
} }
} }

View file

@ -6,10 +6,13 @@ export default class DataExportService extends AbstractService {
return this.post('/user/export/request', {password: password}) return this.post('/user/export/request', {password: password})
} }
download(password) { async download(password) {
const clear = this.setLoading() const clear = this.setLoading()
return this.getBlobUrl('/user/export/download', 'POST', {password}) try {
.then(url => downloadBlob(url, 'vikunja-export.zip')) const url = await this.getBlobUrl('/user/export/download', 'POST', {password})
.finally(() => clear()) downloadBlob(url, 'vikunja-export.zip')
} finally {
clear()
}
} }
} }

View file

@ -44,36 +44,27 @@ export default class ListService extends AbstractService {
return super.update(newModel) return super.update(newModel)
} }
background(list) { async background(list) {
if (list.background === null) { if (list.background === null) {
return Promise.resolve('') return ''
} }
return this.http({ const response = await this.http({
url: `/lists/${list.id}/background`, url: `/lists/${list.id}/background`,
method: 'GET', method: 'GET',
responseType: 'blob', responseType: 'blob',
}) })
.then(response => { return window.URL.createObjectURL(new Blob([response.data]))
return window.URL.createObjectURL(new Blob([response.data]))
})
.catch(e => {
return e
})
} }
removeBackground(list) { async removeBackground(list) {
const cancel = this.setLoading() const cancel = this.setLoading()
return this.http.delete(`/lists/${list.id}/background`, list) try {
.then(response => { const response = await this.http.delete(`/lists/${list.id}/background`, list)
return Promise.resolve(response.data) return response.data
}) } finally {
.catch(error => { cancel()
return this.errorHandler(error) }
})
.finally(() => {
cancel()
})
} }
} }

View file

@ -15,31 +15,23 @@ export default class PasswordResetService extends AbstractService {
return new PasswordResetModel(data) return new PasswordResetModel(data)
} }
resetPassword(model) { async resetPassword(model) {
const cancel = this.setLoading() const cancel = this.setLoading()
return this.http.post(this.paths.reset, model) try {
.catch(error => { const response = await this.http.post(this.paths.reset, model)
return this.errorHandler(error) return this.modelFactory(response.data)
}) } finally {
.then(response => { cancel()
return Promise.resolve(this.modelFactory(response.data)) }
})
.finally(() => {
cancel()
})
} }
requestResetPassword(model) { async requestResetPassword(model) {
const cancel = this.setLoading() const cancel = this.setLoading()
return this.http.post(this.paths.requestReset, model) try {
.catch(error => { const response = await this.http.post(this.paths.requestReset, model)
return this.errorHandler(error) return this.modelFactory(response.data)
}) } finally {
.then(response => { cancel()
return Promise.resolve(this.modelFactory(response.data)) }
})
.finally(() => {
cancel()
})
} }
} }

View file

@ -26,13 +26,12 @@ export default class TotpService extends AbstractService {
return this.post(`${this.urlPrefix}/disable`, model) return this.post(`${this.urlPrefix}/disable`, model)
} }
qrcode() { async qrcode() {
return this.http({ const response = await this.http({
url: `${this.urlPrefix}/qrcode`, url: `${this.urlPrefix}/qrcode`,
method: 'GET', method: 'GET',
responseType: 'blob', responseType: 'blob',
}).then(response => {
return Promise.resolve(new Blob([response.data]))
}) })
return new Blob([response.data])
} }
} }

View file

@ -78,7 +78,7 @@ export default {
}, },
actions: { actions: {
// Logs a user in with a set of credentials. // Logs a user in with a set of credentials.
login(ctx, credentials) { async login(ctx, credentials) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true}) ctx.commit(LOADING, true, {root: true})
@ -94,53 +94,51 @@ export default {
data.totp_passcode = credentials.totpPasscode data.totp_passcode = credentials.totpPasscode
} }
return HTTP.post('login', data) try {
.then(response => { const response = await HTTP.post('login', data)
// Save the token to local storage for later use // Save the token to local storage for later use
saveToken(response.data.token, true) saveToken(response.data.token, true)
// Tell others the user is autheticated // Tell others the user is autheticated
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')
return Promise.resolve() } catch(e) {
}) if (
.catch(e => { e.response &&
if (e.response) { e.response.data.code === 1017 &&
if (e.response.data.code === 1017 && !credentials.totpPasscode) { !credentials.totpPasscode
ctx.commit('needsTotpPasscode', true) ) {
return Promise.reject(e) ctx.commit('needsTotpPasscode', true)
} }
}
return Promise.reject(e) throw e
}) } finally {
.finally(() => { ctx.commit(LOADING, false, {root: true})
ctx.commit(LOADING, false, {root: true}) }
})
}, },
// Registers a new user and logs them in. // Registers a new user and logs them in.
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited. // Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
register(ctx, credentials) { async register(ctx, credentials) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
return HTTP.post('register', { try {
username: credentials.username, await HTTP.post('register', {
email: credentials.email, username: credentials.username,
password: credentials.password, email: credentials.email,
}) password: credentials.password,
.then(() => {
return ctx.dispatch('login', credentials)
}) })
.catch(e => { return ctx.dispatch('login', credentials)
if (e.response && e.response.data && e.response.data.message) { } catch(e) {
ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true}) if (e.response && e.response.data && e.response.data.message) {
} ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true})
}
return Promise.reject(e) throw e
}) } finally {
.finally(() => { ctx.commit(LOADING, false, {root: true})
ctx.commit(LOADING, false, {root: true}) }
})
}, },
openIdAuth(ctx, {provider, code}) {
async openIdAuth(ctx, {provider, code}) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true}) ctx.commit(LOADING, true, {root: true})
@ -150,42 +148,35 @@ export default {
// Delete an eventually preexisting old token // Delete an eventually preexisting old token
removeToken() removeToken()
return HTTP.post(`/auth/openid/${provider}/callback`, data) try {
.then(response => { const response = await HTTP.post(`/auth/openid/${provider}/callback`, data)
// Save the token to local storage for later use // Save the token to local storage for later use
saveToken(response.data.token, true) saveToken(response.data.token, true)
// Tell others the user is autheticated // Tell others the user is autheticated
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')
return Promise.resolve() } finally {
}) ctx.commit(LOADING, false, {root: true})
.catch(e => { }
return Promise.reject(e)
})
.finally(() => {
ctx.commit(LOADING, false, {root: true})
})
}, },
linkShareAuth(ctx, {hash, password}) {
async linkShareAuth(ctx, {hash, password}) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
return HTTP.post('/shares/' + hash + '/auth', { const response = await HTTP.post('/shares/' + hash + '/auth', {
password: password, password: password,
}) })
.then(r => { saveToken(response.data.token, false)
saveToken(r.data.token, false) ctx.dispatch('checkAuth')
ctx.dispatch('checkAuth') return response.data
return Promise.resolve(r.data)
}).catch(e => {
return Promise.reject(e)
})
}, },
// Populates user information from jwt token saved in local storage in store // Populates user information from jwt token saved in local storage in store
checkAuth(ctx) { checkAuth(ctx) {
// This function can be called from multiple places at the same time and shortly after one another. // This function can be called from multiple places at the same time and shortly after one another.
// To prevent hitting the api too frequently or race conditions, we check at most once per minute. // To prevent hitting the api too frequently or race conditions, we check at most once per minute.
if (ctx.state.lastUserInfoRefresh !== null && ctx.state.lastUserInfoRefresh > (new Date()).setMinutes((new Date()).getMinutes() + 1)) { if (ctx.state.lastUserInfoRefresh !== null && ctx.state.lastUserInfoRefresh > (new Date()).setMinutes((new Date()).getMinutes() + 1)) {
return Promise.resolve() return
} }
const jwt = getToken() const jwt = getToken()
@ -195,14 +186,13 @@ export default {
.split('.')[1] .split('.')[1]
.replace('-', '+') .replace('-', '+')
.replace('_', '/') .replace('_', '/')
const info = new UserModel(JSON.parse(window.atob(base64))) const info = new UserModel(JSON.parse(atob(base64)))
const ts = Math.round((new Date()).getTime() / 1000) const ts = Math.round((new Date()).getTime() / 1000)
authenticated = info.exp >= ts authenticated = info.exp >= ts
ctx.commit('info', info) ctx.commit('info', info)
if (authenticated) { if (authenticated) {
ctx.dispatch('refreshUserInfo') ctx.dispatch('refreshUserInfo')
ctx.commit('authenticated', authenticated)
} }
} }
@ -211,57 +201,55 @@ export default {
ctx.commit('info', null) ctx.commit('info', null)
ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true}) ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true})
} }
return Promise.resolve()
}, },
refreshUserInfo(ctx) {
async refreshUserInfo(ctx) {
const jwt = getToken() const jwt = getToken()
if (!jwt) { if (!jwt) {
return return
} }
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
// We're not returning the promise here to prevent blocking the initial ui render if the user is try {
// accessing the site with a token in local storage
HTTP.get('user', {
headers: {
Authorization: `Bearer ${jwt}`,
},
})
.then(r => {
const info = new UserModel(r.data)
info.type = ctx.state.info.type
info.email = ctx.state.info.email
info.exp = ctx.state.info.exp
ctx.commit('info', info) const response = await HTTP.get('user', {
ctx.commit('lastUserRefresh') headers: {
}) Authorization: `Bearer ${jwt}`,
.catch(e => { },
console.error('Error while refreshing user info:', e)
}) })
const info = new UserModel(response.data)
info.type = ctx.state.info.type
info.email = ctx.state.info.email
info.exp = ctx.state.info.exp
ctx.commit('info', info)
ctx.commit('lastUserRefresh')
return info
} catch(e) {
throw new Error('Error while refreshing user info:', { cause: e })
}
}, },
// Renews the api token and saves it to local storage // Renews the api token and saves it to local storage
renewToken(ctx) { renewToken(ctx) {
// Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a // FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
// link share in another tab. Without the timeout both the token renew and link share auth are executed at // link share in another tab. Without the timeout both the token renew and link share auth are executed at
// the same time and one might win over the other. // the same time and one might win over the other.
setTimeout(() => { setTimeout(async () => {
if (!ctx.state.authenticated) { if (!ctx.state.authenticated) {
return return
} }
refreshToken(!ctx.state.isLinkShareAuth) try {
.then(() => { await refreshToken(!ctx.state.isLinkShareAuth)
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')
}) } catch(e) {
.catch(e => { // Don't logout on network errors as the user would then get logged out if they don't have
// Don't logout on network errors as the user would then get logged out if they don't have // internet for a short period of time - such as when the laptop is still reconnecting
// internet for a short period of time - such as when the laptop is still reconnecting if (e.request.status) {
if (e.request.status) { ctx.dispatch('logout')
ctx.dispatch('logout') }
} }
})
}, 5000) }, 5000)
}, },
logout(ctx) { logout(ctx) {

View file

@ -60,16 +60,14 @@ export default {
}, },
}, },
actions: { actions: {
update(ctx) { async update(ctx) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
return HTTP.get('info') const { data: info } = await HTTP.get('info')
.then(r => { ctx.commit(CONFIG, info)
ctx.commit(CONFIG, r.data) return info
return Promise.resolve(r)
})
.catch(e => Promise.reject(e))
}, },
redirectToProviderIfNothingElseIsEnabled(ctx) { redirectToProviderIfNothingElseIsEnabled(ctx) {
if (ctx.state.auth.local.enabled === false && if (ctx.state.auth.local.enabled === false &&
ctx.state.auth.openidConnect.enabled && ctx.state.auth.openidConnect.enabled &&

View file

@ -209,7 +209,7 @@ export default {
}, },
actions: { actions: {
loadBucketsForList(ctx, {listId, params}) { async loadBucketsForList(ctx, {listId, params}) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
@ -218,33 +218,29 @@ export default {
params.per_page = TASKS_PER_BUCKET params.per_page = TASKS_PER_BUCKET
const bucketService = new BucketService() const bucketService = new BucketService()
return bucketService.getAll({listId: listId}, params) try {
.then(r => { const response = await bucketService.getAll({listId: listId}, params)
ctx.commit('setBuckets', r) ctx.commit('setBuckets', response)
ctx.commit('setListId', listId) ctx.commit('setListId', listId)
return Promise.resolve(r) return response
}) } finally {
.catch(e => { cancel()
return Promise.reject(e) }
})
.finally(() => {
cancel()
})
}, },
loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) { async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) {
const bucketIndex = findIndexById(ctx.state.buckets, bucketId) const bucketIndex = findIndexById(ctx.state.buckets, bucketId)
const isLoading = ctx.state.bucketLoading[bucketIndex] ?? false const isLoading = ctx.state.bucketLoading[bucketIndex] ?? false
if (isLoading) { if (isLoading) {
return Promise.resolve() return
} }
const page = (ctx.state.taskPagesPerBucket[bucketIndex] ?? 1) + 1 const page = (ctx.state.taskPagesPerBucket[bucketIndex] ?? 1) + 1
const alreadyLoaded = ctx.state.allTasksLoadedForBucket[bucketIndex] ?? false const alreadyLoaded = ctx.state.allTasksLoadedForBucket[bucketIndex] ?? false
if (alreadyLoaded) { if (alreadyLoaded) {
return Promise.resolve() return
} }
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
@ -275,61 +271,50 @@ export default {
params.per_page = TASKS_PER_BUCKET params.per_page = TASKS_PER_BUCKET
const taskService = new TaskCollectionService() const taskService = new TaskCollectionService()
return taskService.getAll({listId: listId}, params, page) try {
.then(r => {
ctx.commit('addTasksToBucket', {tasks: r, bucketId: bucketId}) const tasks = await taskService.getAll({listId: listId}, params, page)
ctx.commit('setTasksLoadedForBucketPage', {bucketId, page}) ctx.commit('addTasksToBucket', {tasks, bucketId: bucketId})
if (taskService.totalPages <= page) { ctx.commit('setTasksLoadedForBucketPage', {bucketId, page})
ctx.commit('setAllTasksLoadedForBucket', bucketId) if (taskService.totalPages <= page) {
} ctx.commit('setAllTasksLoadedForBucket', bucketId)
return Promise.resolve(r) }
}) return tasks
.catch(e => { } finally {
return Promise.reject(e) cancel()
}) ctx.commit('setBucketLoading', {bucketId: bucketId, loading: false})
.finally(() => { }
cancel()
ctx.commit('setBucketLoading', {bucketId: bucketId, loading: false})
})
}, },
createBucket(ctx, bucket) { async createBucket(ctx, bucket) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketService = new BucketService() const bucketService = new BucketService()
return bucketService.create(bucket) try {
.then(r => { const createdBucket = await bucketService.create(bucket)
ctx.commit('addBucket', r) ctx.commit('addBucket', createdBucket)
return Promise.resolve(r) return createdBucket
}) } finally {
.catch(e => { cancel()
return Promise.reject(e) }
})
.finally(() => {
cancel()
})
}, },
deleteBucket(ctx, {bucket, params}) { async deleteBucket(ctx, {bucket, params}) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketService = new BucketService() const bucketService = new BucketService()
return bucketService.delete(bucket) try {
.then(r => { const response = await bucketService.delete(bucket)
ctx.commit('removeBucket', bucket) ctx.commit('removeBucket', bucket)
// We reload all buckets because tasks are being moved from the deleted bucket // We reload all buckets because tasks are being moved from the deleted bucket
ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params: params}) ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params: params})
return Promise.resolve(r) return response
}) } finally {
.catch(e => { cancel()
return Promise.reject(e) }
})
.finally(() => {
cancel()
})
}, },
updateBucket(ctx, updatedBucketData) { async updateBucket(ctx, updatedBucketData) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id) const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id)
@ -343,21 +328,21 @@ export default {
ctx.commit('setBucketByIndex', {bucketIndex, bucket: updatedBucket}) ctx.commit('setBucketByIndex', {bucketIndex, bucket: updatedBucket})
const bucketService = new BucketService() const bucketService = new BucketService()
return bucketService.update(updatedBucket) try {
.then(r => { const returnedBucket = await bucketService.update(updatedBucket)
ctx.commit('setBucketByIndex', {bucketIndex, bucket: r}) ctx.commit('setBucketByIndex', {bucketIndex, bucket: returnedBucket})
Promise.resolve(r) return returnedBucket
}) } catch(e) {
.catch(e => { // restore original state
// restore original state ctx.commit('setBucketByIndex', {bucketIndex, bucket: oldBucket})
ctx.commit('setBucketByIndex', {bucketIndex, bucket: oldBucket})
return Promise.reject(e) throw e
}) } finally {
.finally(() => cancel()) cancel()
}
}, },
updateBucketTitle(ctx, { id, title }) { async updateBucketTitle(ctx, { id, title }) {
const bucket = findById(ctx.state.buckets, id) const bucket = findById(ctx.state.buckets, id)
if (bucket.title === title) { if (bucket.title === title) {
@ -370,9 +355,8 @@ export default {
title, title,
} }
ctx.dispatch('updateBucket', updatedBucketData).then(() => { await ctx.dispatch('updateBucket', updatedBucketData)
success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')})
})
}, },
}, },
} }

View file

@ -1,15 +1,18 @@
import LabelService from '@/services/label' import LabelService from '@/services/label'
import {setLoading} from '@/store/helper' import {setLoading} from '@/store/helper'
import {filterLabelsByQuery} from '@/helpers/labels' import { success } from '@/message'
import {i18n} from '@/i18n'
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
/** async function getAllLabels(page = 1) {
* Returns the labels by id if found const labelService = new LabelService()
* @param {Object} state const labels = await labelService.getAll({}, {}, page)
* @param {Array} ids if (page < labelService.totalPages) {
* @returns {Array} const nextLabels = await getAllLabels(page + 1)
*/ return labels.concat(nextLabels)
function getLabelsByIds(state, ids) { } else {
return Object.values(state.labels).filter(({id}) => ids.includes(id)) return labels
}
} }
export default { export default {
@ -44,75 +47,59 @@ export default {
}, },
}, },
actions: { actions: {
loadAllLabels(ctx, {forceLoad} = {}) { async loadAllLabels(ctx, {forceLoad} = {}) {
if (ctx.state.loaded && !forceLoad) { if (ctx.state.loaded && !forceLoad) {
return Promise.resolve() return
} }
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()
const getAllLabels = (page = 1) => { try {
return labelService.getAll({}, {}, page) const labels = await getAllLabels()
.then(labels => { ctx.commit('setLabels', labels)
if (page < labelService.totalPages) { ctx.commit('setLoaded', true)
return getAllLabels(page + 1) return labels
.then(nextLabels => { } finally {
return labels.concat(nextLabels) cancel()
})
} else {
return labels
}
})
.catch(e => {
return Promise.reject(e)
})
} }
return getAllLabels()
.then(r => {
ctx.commit('setLabels', r)
ctx.commit('setLoaded', true)
return Promise.resolve(r)
})
.catch(e => Promise.reject(e))
.finally(() => cancel())
}, },
deleteLabel(ctx, label) { async deleteLabel(ctx, label) {
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService() const labelService = new LabelService()
return labelService.delete(label) try {
.then(r => { const result = await labelService.delete(label)
ctx.commit('removeLabelById', label) ctx.commit('removeLabelById', label)
return Promise.resolve(r) success({message: i18n.global.t('label.deleteSuccess')})
}) return result
.catch(e => Promise.reject(e)) } finally {
.finally(() => cancel()) cancel()
}
}, },
updateLabel(ctx, label) { async updateLabel(ctx, label) {
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService() const labelService = new LabelService()
return labelService.update(label) try {
.then(r => { const newLabel = await labelService.update(label)
ctx.commit('setLabel', r) ctx.commit('setLabel', newLabel)
return Promise.resolve(r) success({message: i18n.global.t('label.edit.success')})
}) return newLabel
.catch(e => Promise.reject(e)) } finally {
.finally(() => cancel()) cancel()
}
}, },
createLabel(ctx, label) { async createLabel(ctx, label) {
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService() const labelService = new LabelService()
return labelService.create(label) try {
.then(r => { const newLabel = await labelService.create(label)
ctx.commit('setLabel', r) ctx.commit('setLabel', newLabel)
return Promise.resolve(r) return newLabel
}) } finally {
.catch(e => Promise.reject(e)) cancel()
.finally(() => cancel()) }
}, },
}, },
} }

View file

@ -42,69 +42,70 @@ export default {
isFavorite: !list.isFavorite, isFavorite: !list.isFavorite,
}) })
}, },
createList(ctx, list) {
async createList(ctx, list) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
const listService = new ListService() const listService = new ListService()
return listService.create(list) try {
.then(r => { const createdList = await listService.create(list)
r.namespaceId = list.namespaceId createdList.namespaceId = list.namespaceId
ctx.commit('namespaces/addListToNamespace', r, {root: true}) ctx.commit('namespaces/addListToNamespace', createdList, {root: true})
ctx.commit('setList', r) ctx.commit('setList', createdList)
return Promise.resolve(r) return createdList
}) } finally {
.catch(e => Promise.reject(e)) cancel()
.finally(() => cancel()) }
}, },
updateList(ctx, list) {
async updateList(ctx, list) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
const listService = new ListService() const listService = new ListService()
return listService.update(list) try {
.then(() => { await listService.update(list)
ctx.commit('setList', list) ctx.commit('setList', list)
ctx.commit('namespaces/setListInNamespaceById', list, {root: true}) ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
// the returned list from listService.update is the same! // the returned list from listService.update is the same!
// in order to not validate vuex mutations we have to create a new copy // in order to not validate vuex mutations we have to create a new copy
const newList = { const newList = {
...list, ...list,
namespaceId: FavoriteListsNamespace, namespaceId: FavoriteListsNamespace,
} }
if (list.isFavorite) { if (list.isFavorite) {
ctx.commit('namespaces/addListToNamespace', newList, {root: true}) ctx.commit('namespaces/addListToNamespace', newList, {root: true})
} else { } else {
ctx.commit('namespaces/removeListFromNamespaceById', newList, {root: true}) ctx.commit('namespaces/removeListFromNamespaceById', newList, {root: true})
} }
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true}) ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true}) ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
return Promise.resolve(newList) return newList
} catch(e) {
// Reset the list state to the initial one to avoid confusion for the user
ctx.commit('setList', {
...list,
isFavorite: !list.isFavorite,
}) })
.catch(e => { throw e
// Reset the list state to the initial one to avoid confusion for the user } finally {
ctx.commit('setList', { cancel()
...list, }
isFavorite: !list.isFavorite,
})
return Promise.reject(e)
})
.finally(() => cancel())
}, },
deleteList(ctx, list) {
async deleteList(ctx, list) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
const listService = new ListService() const listService = new ListService()
return listService.delete(list) try {
.then(r => { const response = await listService.delete(list)
ctx.commit('removeListById', list) ctx.commit('removeListById', list)
ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true}) ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true})
removeListFromHistory({id: list.id}) removeListFromHistory({id: list.id})
return Promise.resolve(r) return response
}) } finally{
.catch(e => { cancel()
return Promise.reject(e) }
})
.finally(() => cancel())
}, },
}, },
} }

View file

@ -94,67 +94,63 @@ export default {
}, },
}, },
actions: { actions: {
loadNamespaces(ctx) { async loadNamespaces(ctx) {
const cancel = setLoading(ctx, 'namespaces') const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
// We always load all namespaces and filter them on the frontend try {
return namespaceService.getAll({}, {is_archived: true}) // We always load all namespaces and filter them on the frontend
.then(r => { const namespaces = await namespaceService.getAll({}, {is_archived: true})
ctx.commit('namespaces', r) ctx.commit('namespaces', namespaces)
// Put all lists in the list state // Put all lists in the list state
const lists = [] const lists = namespaces.flatMap(({lists}) => lists)
r.forEach(n => {
n.lists.forEach(l => {
lists.push(l)
})
})
ctx.commit('lists/setLists', lists, {root: true}) ctx.commit('lists/setLists', lists, {root: true})
return Promise.resolve(r) return namespaces
}) } finally {
.catch(e => Promise.reject(e)) cancel()
.finally(() => { }
cancel()
})
}, },
loadNamespacesIfFavoritesDontExist(ctx) { loadNamespacesIfFavoritesDontExist(ctx) {
// The first namespace should be the one holding all favorites // The first namespace should be the one holding all favorites
if (ctx.state.namespaces[0].id !== -2) { if (ctx.state.namespaces[0].id !== -2) {
return ctx.dispatch('loadNamespaces') return ctx.dispatch('loadNamespaces')
} }
}, },
removeFavoritesNamespaceIfEmpty(ctx) { removeFavoritesNamespaceIfEmpty(ctx) {
if (ctx.state.namespaces[0].id === -2 && ctx.state.namespaces[0].lists.length === 0) { if (ctx.state.namespaces[0].id === -2 && ctx.state.namespaces[0].lists.length === 0) {
ctx.state.namespaces.splice(0, 1) ctx.state.namespaces.splice(0, 1)
return Promise.resolve()
} }
}, },
deleteNamespace(ctx, namespace) {
async deleteNamespace(ctx, namespace) {
const cancel = setLoading(ctx, 'namespaces') const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
return namespaceService.delete(namespace) try {
.then(r => { const response = await namespaceService.delete(namespace)
ctx.commit('removeNamespaceById', namespace.id) ctx.commit('removeNamespaceById', namespace.id)
return Promise.resolve(r) return response
}) } finally {
.catch(e => Promise.reject(e)) cancel()
.finally(() => cancel()) }
}, },
createNamespace(ctx, namespace) {
async createNamespace(ctx, namespace) {
const cancel = setLoading(ctx, 'namespaces') const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
return namespaceService.create(namespace) try {
.then(r => { const createdNamespace = await namespaceService.create(namespace)
ctx.commit('addNamespace', r) ctx.commit('addNamespace', createdNamespace)
return Promise.resolve(r) return createdNamespace
}) } finally {
.catch(e => Promise.reject(e)) cancel()
.finally(() => cancel()) }
}, },
}, },
} }

View file

@ -34,33 +34,30 @@ function validateLabel(labels, label) {
return findPropertyByValue(labels, 'title', label) return findPropertyByValue(labels, 'title', label)
} }
function addLabelToTask(task, label) { async function addLabelToTask(task, label) {
const labelTask = new LabelTask({ const labelTask = new LabelTask({
taskId: task.id, taskId: task.id,
labelId: label.id, labelId: label.id,
}) })
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
return labelTaskService.create(labelTask) const response = await labelTaskService.create(labelTask)
.then(result => { task.labels.push(label)
task.labels.push(label) return response
return Promise.resolve(result)
})
.catch(e => Promise.reject(e))
} }
function findAssignees(parsedTaskAssignees) { async function findAssignees(parsedTaskAssignees) {
if (parsedTaskAssignees.length <= 0) { if (parsedTaskAssignees.length <= 0) {
return Promise.resolve([]) return []
} }
const userService = new UserService() const userService = new UserService()
const assignees = parsedTaskAssignees.map(a => const assignees = parsedTaskAssignees.map(async a => {
userService.getAll({}, {s: a}) const users = await userService.getAll({}, {s: a})
.then(users => validateUsername(users, a)), return validateUsername(users, a)
})
) const validatedUsers = await Promise.all(assignees)
return validatedUsers.filter((item) => Boolean(item))
return Promise.all(assignees).filter((item) => Boolean(item))
} }
@ -68,50 +65,39 @@ export default {
namespaced: true, namespaced: true,
state: () => ({}), state: () => ({}),
actions: { actions: {
loadTasks(ctx, params) { async loadTasks(ctx, params) {
const taskService = new TaskService() const taskService = new TaskService()
const cancel = setLoading(ctx, 'tasks') const cancel = setLoading(ctx, 'tasks')
return taskService.getAll({}, params) try {
.then(r => { const tasks = await taskService.getAll({}, params)
ctx.commit(HAS_TASKS, r.length > 0, {root: true}) ctx.commit(HAS_TASKS, tasks.length > 0, {root: true})
return r return tasks
}) } finally {
.catch(e => { cancel()
return Promise.reject(e) }
})
.finally(() => {
cancel()
})
}, },
update(ctx, task) {
async update(ctx, task) {
const cancel = setLoading(ctx, 'tasks') const cancel = setLoading(ctx, 'tasks')
const taskService = new TaskService() const taskService = new TaskService()
return taskService.update(task) try {
.then(t => { const updatedTask = await taskService.update(task)
ctx.commit('kanban/setTaskInBucket', t, {root: true}) ctx.commit('kanban/setTaskInBucket', updatedTask, {root: true})
return Promise.resolve(t) return updatedTask
}) } finally {
.catch(e => { cancel()
return Promise.reject(e) }
})
.finally(() => {
cancel()
})
}, },
delete(ctx, task) {
async delete(ctx, task) {
const taskService = new TaskService() const taskService = new TaskService()
return taskService.delete(task) const response = await taskService.delete(task)
.then(t => { ctx.commit('kanban/removeTaskInBucket', task, {root: true})
ctx.commit('kanban/removeTaskInBucket', task, {root: true}) return response
return Promise.resolve(t)
})
.catch(e => {
return Promise.reject(e)
})
}, },
// Adds a task attachment in store. // Adds a task attachment in store.
// This is an action to be able to commit other mutations // This is an action to be able to commit other mutations
addTaskAttachment(ctx, {taskId, attachment}) { addTaskAttachment(ctx, {taskId, attachment}) {
@ -134,118 +120,97 @@ export default {
ctx.commit('attachments/add', attachment, {root: true}) ctx.commit('attachments/add', attachment, {root: true})
}, },
addAssignee(ctx, {user, taskId}) { async addAssignee(ctx, {user, taskId}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId}) const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
return taskAssigneeService.create(taskAssignee) const r = await taskAssigneeService.create(taskAssignee)
.then(r => { const t = ctx.rootGetters['kanban/getTaskById'](taskId)
const t = ctx.rootGetters['kanban/getTaskById'](taskId) if (t.task === null) {
if (t.task === null) { // Don't try further adding a label if the task is not in kanban
// Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now.
// Usually this means the kanban board hasn't been accessed until now. // Vuex seems to have its difficulties with that, so we just log the error and fail silently.
// Vuex seems to have its difficulties with that, so we just log the error and fail silently. console.debug('Could not add assignee to task in kanban, task not found', t)
console.debug('Could not add assignee to task in kanban, task not found', t) return r
return Promise.resolve(r) }
} // FIXME: direct store manipulation (task)
// FIXME: direct store manipulation (task) t.task.assignees.push(user)
t.task.assignees.push(user) ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true })
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) return r
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
}, },
removeAssignee(ctx, {user, taskId}) {
async removeAssignee(ctx, {user, taskId}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId}) const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
return taskAssigneeService.delete(taskAssignee) const response = await taskAssigneeService.delete(taskAssignee)
.then(r => { const t = ctx.rootGetters['kanban/getTaskById'](taskId)
const t = ctx.rootGetters['kanban/getTaskById'](taskId) if (t.task === null) {
if (t.task === null) { // Don't try further adding a label if the task is not in kanban
// Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now.
// Usually this means the kanban board hasn't been accessed until now. // Vuex seems to have its difficulties with that, so we just log the error and fail silently.
// Vuex seems to have its difficulties with that, so we just log the error and fail silently. console.debug('Could not remove assignee from task in kanban, task not found', t)
console.debug('Could not remove assignee from task in kanban, task not found', t) return response
return Promise.resolve(r) }
}
for (const a in t.task.assignees) { for (const a in t.task.assignees) {
if (t.task.assignees[a].id === user.id) { if (t.task.assignees[a].id === user.id) {
// FIXME: direct store manipulation (task)
t.task.assignees.splice(a, 1)
break
}
}
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
},
addLabel(ctx, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
const labelTaskService = new LabelTaskService()
return labelTaskService.create(labelTask)
.then(r => {
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
// Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now.
// Vuex seems to have its difficulties with that, so we just log the error and fail silently.
console.debug('Could not add label to task in kanban, task not found', t)
return Promise.resolve(r)
}
// FIXME: direct store manipulation (task) // FIXME: direct store manipulation (task)
t.task.labels.push(label) t.task.assignees.splice(a, 1)
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) break
}
}
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return response
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
}, },
removeLabel(ctx, {label, taskId}) { async addLabel(ctx, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id}) const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
return labelTaskService.delete(labelTask) const r = await labelTaskService.create(labelTask)
.then(r => { const t = ctx.rootGetters['kanban/getTaskById'](taskId)
const t = ctx.rootGetters['kanban/getTaskById'](taskId) if (t.task === null) {
if (t.task === null) { // Don't try further adding a label if the task is not in kanban
// Don't try further adding a label if the task is not in kanban // Usually this means the kanban board hasn't been accessed until now.
// Usually this means the kanban board hasn't been accessed until now. // Vuex seems to have its difficulties with that, so we just log the error and fail silently.
// Vuex seems to have its difficulties with that, so we just log the error and fail silently. console.debug('Could not add label to task in kanban, task not found', t)
console.debug('Could not remove label from task in kanban, task not found', t) return r
return Promise.resolve(r) }
} // FIXME: direct store manipulation (task)
t.task.labels.push(label)
ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true })
return r
},
// Remove the label from the list async removeLabel(ctx, {label, taskId}) {
for (const l in t.task.labels) { const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
if (t.task.labels[l].id === label.id) {
// FIXME: direct store manipulation (task)
t.task.labels.splice(l, 1)
break
}
}
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) const labelTaskService = new LabelTaskService()
const response = await labelTaskService.delete(labelTask)
const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task === null) {
// Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now.
// Vuex seems to have its difficulties with that, so we just log the error and fail silently.
console.debug('Could not remove label from task in kanban, task not found', t)
return response
}
return Promise.resolve(r) // Remove the label from the list
}) for (const l in t.task.labels) {
.catch(e => { if (t.task.labels[l].id === label.id) {
return Promise.reject(e) // FIXME: direct store manipulation (task)
}) t.task.labels.splice(l, 1)
break
}
}
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return response
}, },
// Do everything that is involved in finding, creating and adding the label to the task // Do everything that is involved in finding, creating and adding the label to the task
@ -256,22 +221,21 @@ export default {
const {labels} = rootState.labels const {labels} = rootState.labels
const labelAddsToWaitFor = parsedLabels.map(labelTitle => new Promise((resolve) => { const labelAddsToWaitFor = parsedLabels.map(async labelTitle => {
let label = validateLabel(labels, labelTitle) let label = validateLabel(labels, labelTitle)
if (typeof label !== 'undefined') { if (typeof label !== 'undefined') {
return resolve(label) return label
} }
// label not found, create it // label not found, create it
const labelModel = new LabelModel({title: labelTitle}) const labelModel = new LabelModel({title: labelTitle})
return dispatch('labels/createLabel', labelModel, {root: true}).then(() => resolve(label)) await dispatch('labels/createLabel', labelModel, {root: true})
return addLabelToTask(task, label)
}) })
.then((label) => addLabelToTask(task, label))
.catch(e => Promise.reject(e)),
)
// This waits until all labels are created and added to the task // This waits until all labels are created and added to the task
return Promise.all(labelAddsToWaitFor).then(() => task) await Promise.all(labelAddsToWaitFor)
return task
}, },
findListId({ rootGetters }, { list: listName, listId }) { findListId({ rootGetters }, { list: listName, listId }) {
@ -296,7 +260,7 @@ export default {
// 4. If none of the above worked, reject the promise with an error. // 4. If none of the above worked, reject the promise with an error.
if (typeof foundListId === 'undefined' || listId === null) { if (typeof foundListId === 'undefined' || listId === null) {
return Promise.reject('NO_LIST') throw new Error('NO_LIST')
} }
return foundListId return foundListId
@ -331,12 +295,11 @@ export default {
}) })
const taskService = new TaskService() const taskService = new TaskService()
return taskService.create(task) const createdTask = await taskService.create(task)
.then(task => dispatch('addLabelsToTask', { return dispatch('addLabelsToTask', {
task, task: createdTask,
parsedLabels: parsedTask.labels, parsedLabels: parsedTask.labels,
})) })
.catch(e => Promise.reject(e))
}, },
}, },
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="content has-text-centered"> <div class="content has-text-centered">
<h2> <h2 v-if="userInfo">
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}! {{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2> </h2>
<div class="notification is-danger" v-if="deletionScheduledAt !== null"> <div class="notification is-danger" v-if="deletionScheduledAt !== null">

View file

@ -93,14 +93,11 @@ export default {
this.$nextTick(() => this.editorActive = true) this.$nextTick(() => this.editorActive = true)
}, },
methods: { methods: {
create() { async create() {
this.savedFilter.filters = this.filters this.savedFilter.filters = this.filters
this.savedFilterService.create(this.savedFilter) const savedFilter = await this.savedFilterService.create(this.savedFilter)
.then(r => { await this.$store.dispatch('namespaces/loadNamespaces')
this.$store.dispatch('namespaces/loadNamespaces') this.$router.push({name: 'list.index', params: {listId: savedFilter.getListId()}})
this.$router.push({name: 'list.index', params: {listId: r.getListId()}})
})
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -24,18 +24,15 @@ export default {
} }
}, },
methods: { methods: {
deleteSavedFilter() { async deleteSavedFilter() {
// We assume the listId in the route is the pseudolist // We assume the listId in the route is the pseudolist
const list = new ListModel({id: this.$route.params.listId}) const list = new ListModel({id: this.$route.params.listId})
const filter = new SavedFilterModel({id: list.getSavedFilterId()}) const filter = new SavedFilterModel({id: list.getSavedFilterId()})
this.filterService.delete(filter) await this.filterService.delete(filter)
.then(() => { await this.$store.dispatch('namespaces/loadNamespaces')
this.$store.dispatch('namespaces/loadNamespaces') this.$message.success({message: this.$t('filters.delete.success')})
this.$message.success({message: this.$t('filters.delete.success')}) this.$router.push({name: 'namespaces.index'})
this.$router.push({name: 'namespaces.index'})
})
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -95,29 +95,22 @@ export default {
}, },
}, },
methods: { methods: {
loadSavedFilter() { async loadSavedFilter() {
// We assume the listId in the route is the pseudolist // We assume the listId in the route is the pseudolist
const list = new ListModel({id: this.$route.params.listId}) const list = new ListModel({id: this.$route.params.listId})
this.filter = new SavedFilterModel({id: list.getSavedFilterId()}) this.filter = new SavedFilterModel({id: list.getSavedFilterId()})
this.filterService.get(this.filter) this.filter = await this.filterService.get(this.filter)
.then(r => { this.filters = objectToSnakeCase(this.filter.filters)
this.filter = r
this.filters = objectToSnakeCase(this.filter.filters)
})
.catch(e => this.$message.error(e))
}, },
save() { async save() {
this.filter.filters = this.filters this.filter.filters = this.filters
this.filterService.update(this.filter) const filter = await this.filterService.update(this.filter)
.then(r => { await this.$store.dispatch('namespaces/loadNamespaces')
this.$store.dispatch('namespaces/loadNamespaces') this.$message.success({message: this.$t('filters.edit.success')})
this.$message.success({message: this.$t('filters.edit.success')}) this.filter = filter
this.filter = r this.filters = objectToSnakeCase(this.filter.filters)
this.filters = objectToSnakeCase(this.filter.filters) this.$router.back()
this.$router.back()
})
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -119,7 +119,7 @@ export default {
} }
}, },
created() { created() {
this.loadLabels() this.$store.dispatch('labels/loadAllLabels')
}, },
mounted() { mounted() {
this.setTitle(this.$t('label.title')) this.setTitle(this.$t('label.title'))
@ -131,29 +131,11 @@ export default {
loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels', loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels',
}), }),
methods: { methods: {
loadLabels() {
this.$store.dispatch('labels/loadAllLabels')
.catch(e => {
this.$message.error(e)
})
},
deleteLabel(label) { deleteLabel(label) {
this.$store.dispatch('labels/deleteLabel', label) return this.$store.dispatch('labels/deleteLabel', label)
.then(() => {
this.$message.success({message: this.$t('label.deleteSuccess')})
})
.catch(e => {
this.$message.error(e)
})
}, },
editLabelSubmit() { editLabelSubmit() {
this.$store.dispatch('labels/updateLabel', this.labelEditLabel) return this.$store.dispatch('labels/updateLabel', this.labelEditLabel)
.then(() => {
this.$message.success({message: this.$t('label.edit.success')})
})
.catch(e => {
this.$message.error(e)
})
}, },
editLabel(label) { editLabel(label) {
if (label.createdBy.id !== this.userInfo.id) { if (label.createdBy.id !== this.userInfo.id) {

View file

@ -60,24 +60,19 @@ export default {
loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels', loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels',
}), }),
methods: { methods: {
newLabel() { async newLabel() {
if (this.label.title === '') { if (this.label.title === '') {
this.showError = true this.showError = true
return return
} }
this.showError = false this.showError = false
this.$store.dispatch('labels/createLabel', this.label) const label = this.$store.dispatch('labels/createLabel', this.label)
.then(r => { this.$router.push({
this.$router.push({ name: 'labels.index',
name: 'labels.index', params: {id: label.id},
params: {id: r.id}, })
}) this.$message.success({message: this.$t('label.create.success')})
this.$message.success({message: this.$t('label.create.success')})
})
.catch((e) => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -54,7 +54,7 @@ export default {
this.setTitle(this.$t('list.create.header')) this.setTitle(this.$t('list.create.header'))
}, },
methods: { methods: {
newList() { async newList() {
if (this.list.title === '') { if (this.list.title === '') {
this.showError = true this.showError = true
return return
@ -62,18 +62,12 @@ export default {
this.showError = false this.showError = false
this.list.namespaceId = parseInt(this.$route.params.id) this.list.namespaceId = parseInt(this.$route.params.id)
this.$store const list = await this.$store.dispatch('lists/createList', this.list)
.dispatch('lists/createList', this.list) this.$message.success({message: this.$t('list.create.createdSuccess') })
.then((r) => { this.$router.push({
this.$message.success({message: this.$t('list.create.createdSuccess') }) name: 'list.index',
this.$router.push({ params: { listId: list.id },
name: 'list.index', })
params: { listId: r.id },
})
})
.catch((e) => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -51,9 +51,6 @@ export default {
listLoaded: 0, listLoaded: 0,
} }
}, },
mounted() {
this.loadList()
},
watch: { watch: {
// call again the method if the route changes // call again the method if the route changes
'$route.path': { '$route.path': {
@ -83,7 +80,8 @@ export default {
this.$router.replace({name: savedListView, params: {id: this.$route.params.listId}}) this.$router.replace({name: savedListView, params: {id: this.$route.params.listId}})
console.debug('Replaced list view with', savedListView) console.debug('Replaced list view with', savedListView)
}, },
loadList() {
async loadList() {
if (this.$route.name.includes('.settings.')) { if (this.$route.name.includes('.settings.')) {
return return
} }
@ -139,17 +137,13 @@ export default {
// 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. // 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.
const list = new ListModel(listData) const list = new ListModel(listData)
this.listService.get(list) try {
.then(r => { const loadedList = await this.listService.get(list)
this.$store.dispatch(CURRENT_LIST, r) await this.$store.dispatch(CURRENT_LIST, loadedList)
this.setTitle(this.getListTitle(r)) this.setTitle(this.getListTitle(loadedList))
}) } finally {
.catch(e => { this.listLoaded = this.$route.params.listId
this.$message.error(e) }
})
.finally(() => {
this.listLoaded = this.$route.params.listId
})
}, },
}, },
} }

View file

@ -30,24 +30,20 @@ export default {
}, },
}, },
methods: { methods: {
archiveList() { async archiveList() {
const newList = { const newList = {
...this.list, ...this.list,
isArchived: !this.list.isArchived, isArchived: !this.list.isArchived,
} }
this.listService.update(newList) try {
.then(r => { const list = await this.listService.update(newList)
this.$store.commit('currentList', r) this.$store.commit('currentList', list)
this.$store.commit('namespaces/setListInNamespaceById', r) this.$store.commit('namespaces/setListInNamespaceById', list)
this.$message.success({message: this.$t('list.archive.success')}) this.$message.success({message: this.$t('list.archive.success')})
}) } finally {
.catch(e => { this.$router.back()
this.$message.error(e) }
})
.finally(() => {
this.$router.back()
})
}, },
}, },
} }

View file

@ -28,7 +28,7 @@
<template v-if="unsplashBackgroundEnabled"> <template v-if="unsplashBackgroundEnabled">
<input <input
:class="{'is-loading': backgroundService.loading}" :class="{'is-loading': backgroundService.loading}"
@keyup="() => newBackgroundSearch()" @keyup="() => debounceNewBackgroundSearch()"
class="input is-expanded" class="input is-expanded"
:placeholder="$t('list.background.searchPlaceholder')" :placeholder="$t('list.background.searchPlaceholder')"
type="text" type="text"
@ -70,18 +70,25 @@ import BackgroundUploadService from '../../../services/backgroundUpload'
import ListService from '@/services/list' import ListService from '@/services/list'
import {CURRENT_LIST} from '@/store/mutation-types' import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit.vue' import CreateEdit from '@/components/misc/create-edit.vue'
import debounce from 'lodash.debounce'
const SEARCH_DEBOUNCE = 300
export default { export default {
name: 'list-setting-background', name: 'list-setting-background',
components: {CreateEdit}, components: {CreateEdit},
data() { data() {
return { return {
backgroundService: new BackgroundUnsplashService(),
backgroundSearchTerm: '', backgroundSearchTerm: '',
backgroundSearchResult: [], backgroundSearchResult: [],
backgroundService: new BackgroundUnsplashService(),
backgroundThumbs: {}, backgroundThumbs: {},
currentPage: 1, currentPage: 1,
backgroundSearchTimeout: null,
// We're using debounce to not search on every keypress but with a delay.
debounceNewBackgroundSearch: debounce(this.newBackgroundSearch, SEARCH_DEBOUNCE, {
trailing: true,
}),
backgroundUploadService: new BackgroundUploadService(), backgroundUploadService: new BackgroundUploadService(),
listService: new ListService(), listService: new ListService(),
@ -108,73 +115,45 @@ export default {
this.backgroundThumbs = {} this.backgroundThumbs = {}
this.searchBackgrounds() this.searchBackgrounds()
}, },
searchBackgrounds(page = 1) {
if (this.backgroundSearchTimeout !== null) { async searchBackgrounds(page = 1) {
clearTimeout(this.backgroundSearchTimeout) this.currentPage = page
} const result = await this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
this.backgroundSearchResult = this.backgroundSearchResult.concat(result)
// We're using the timeout to not search on every keypress but with a 300ms delay. result.forEach(async background => {
// If another key is pressed within these 300ms, the last search request is dropped and a new one is scheduled. this.backgroundThumbs[background.id] = await this.backgroundService.thumb(background)
this.backgroundSearchTimeout = setTimeout(() => { })
this.currentPage = page
this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
.then(r => {
this.backgroundSearchResult = this.backgroundSearchResult.concat(r)
r.forEach(b => {
this.backgroundService.thumb(b)
.then(t => {
this.backgroundThumbs[b.id] = t
})
})
})
.catch(e => {
this.$message.error(e)
})
}, 300)
}, },
setBackground(backgroundId) {
async setBackground(backgroundId) {
// Don't set a background if we're in the process of setting one // Don't set a background if we're in the process of setting one
if (this.backgroundService.loading) { if (this.backgroundService.loading) {
return return
} }
this.backgroundService.update({id: backgroundId, listId: this.$route.params.listId}) const list = await this.backgroundService.update({id: backgroundId, listId: this.$route.params.listId})
.then(l => { await this.$store.dispatch(CURRENT_LIST, list)
this.$store.commit(CURRENT_LIST, l) this.$store.commit('namespaces/setListInNamespaceById', list)
this.$store.commit('namespaces/setListInNamespaceById', l) this.$message.success({message: this.$t('list.background.success')})
this.$message.success({message: this.$t('list.background.success')})
})
.catch(e => {
this.$message.error(e)
})
}, },
uploadBackground() {
async uploadBackground() {
if (this.$refs.backgroundUploadInput.files.length === 0) { if (this.$refs.backgroundUploadInput.files.length === 0) {
return return
} }
this.backgroundUploadService.create(this.$route.params.listId, this.$refs.backgroundUploadInput.files[0]) const list = await this.backgroundUploadService.create(this.$route.params.listId, this.$refs.backgroundUploadInput.files[0])
.then(l => { await this.$store.dispatch(CURRENT_LIST, list)
this.$store.commit(CURRENT_LIST, l) this.$store.commit('namespaces/setListInNamespaceById', list)
this.$store.commit('namespaces/setListInNamespaceById', l) this.$message.success({message: this.$t('list.background.success')})
this.$message.success({message: this.$t('list.background.success')})
})
.catch(e => {
this.$message.error(e)
})
}, },
removeBackground() {
this.listService.removeBackground(this.currentList) async removeBackground() {
.then(l => { const list = await this.listService.removeBackground(this.currentList)
this.$store.commit(CURRENT_LIST, l) await this.$store.dispatch(CURRENT_LIST, list)
this.$store.commit('namespaces/setListInNamespaceById', l) this.$store.commit('namespaces/setListInNamespaceById', list)
this.$message.success({message: this.$t('list.background.removeSuccess')}) this.$message.success({message: this.$t('list.background.removeSuccess')})
this.$router.back() this.$router.back()
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -24,15 +24,10 @@ export default {
}, },
}, },
methods: { methods: {
deleteList() { async deleteList() {
this.$store.dispatch('lists/deleteList', this.list) await this.$store.dispatch('lists/deleteList', this.list)
.then(() => { this.$message.success({message: this.$t('list.delete.success')})
this.$message.success({message: this.$t('list.delete.success')}) this.$router.push({name: 'home'})
this.$router.push({name: 'home'})
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -38,21 +38,17 @@ export default {
selectNamespace(namespace) { selectNamespace(namespace) {
this.selectedNamespace = namespace this.selectedNamespace = namespace
}, },
duplicateList() {
async duplicateList() {
const listDuplicate = new ListDuplicateModel({ const listDuplicate = new ListDuplicateModel({
listId: this.$route.params.listId, listId: this.$route.params.listId,
namespaceId: this.selectedNamespace.id, namespaceId: this.selectedNamespace.id,
}) })
this.listDuplicateService.create(listDuplicate) const duplicate = await this.listDuplicateService.create(listDuplicate)
.then(r => { this.$store.commit('namespaces/addListToNamespace', duplicate.list)
this.$store.commit('namespaces/addListToNamespace', r.list) this.$store.commit('lists/setList', duplicate.list)
this.$store.commit('lists/setList', r.list) this.$message.success({message: this.$t('list.duplicate.success')})
this.$message.success({message: this.$t('list.duplicate.success')}) this.$router.push({name: 'list.index', params: {listId: duplicate.list.id}})
this.$router.push({name: 'list.index', params: {listId: r.list.id}})
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -94,28 +94,19 @@ export default {
}, },
}, },
methods: { methods: {
loadList() { async loadList() {
const list = new ListModel({id: this.$route.params.listId}) const list = new ListModel({id: this.$route.params.listId})
this.listService.get(list) const loadedList = await this.listService.get(list)
.then(r => { this.list = { ...loadedList }
this.list = { ...r }
})
.catch(e => {
this.$message.error(e)
})
}, },
save() {
this.$store.dispatch('lists/updateList', this.list) async save() {
.then(() => { await this.$store.dispatch('lists/updateList', this.list)
this.$store.commit(CURRENT_LIST, this.list) await this.$store.dispatch(CURRENT_LIST, this.list)
this.setTitle(this.$t('list.edit.title', {list: this.list.title})) this.setTitle(this.$t('list.edit.title', {list: this.list.title}))
this.$message.success({message: this.$t('list.edit.success')}) this.$message.success({message: this.$t('list.edit.success')})
this.$router.back() this.$router.back()
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -56,21 +56,15 @@ export default {
this.loadList() this.loadList()
}, },
methods: { methods: {
loadList() { async loadList() {
const list = new ListModel({id: this.$route.params.listId}) const list = new ListModel({id: this.$route.params.listId})
this.listService.get(list) this.list = await this.listService.get(list)
.then(r => { await this.$store.dispatch(CURRENT_LIST, this.list)
this.list = r // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.$store.commit(CURRENT_LIST, r) this.manageTeamsComponent = 'userTeam'
// This will trigger the dynamic loading of components once we actually have all the data to pass to them this.manageUsersComponent = 'userTeam'
this.manageTeamsComponent = 'userTeam' this.setTitle(this.$t('list.share.title', {list: this.list.title}))
this.manageUsersComponent = 'userTeam'
this.setTitle(this.$t('list.share.title', {list: this.list.title}))
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -409,7 +409,7 @@ export default {
this.$store.commit('kanban/setBucketById', newBucket) this.$store.commit('kanban/setBucketById', newBucket)
}, },
updateTaskPosition(e) { async updateTaskPosition(e) {
this.drag = false this.drag = false
// While we could just pass the bucket index in through the function call, this would not give us the // While we could just pass the bucket index in through the function call, this would not give us the
@ -424,36 +424,37 @@ export default {
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
newTask.bucketId = newBucket.id, newTask.bucketId = newBucket.id,
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null), newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
this.$store.dispatch('tasks/update', newTask) try {
.finally(() => { await this.$store.dispatch('tasks/update', newTask)
this.taskUpdating[task.id] = false } finally {
this.oneTaskUpdating = false this.taskUpdating[task.id] = false
}) this.oneTaskUpdating = false
}
}, },
toggleShowNewTaskInput(bucketId) { toggleShowNewTaskInput(bucketId) {
this.showNewTaskInput[bucketId] = !this.showNewTaskInput[bucketId] this.showNewTaskInput[bucketId] = !this.showNewTaskInput[bucketId]
}, },
addTaskToBucket(bucketId) {
async addTaskToBucket(bucketId) {
if (this.newTaskText === '') { if (this.newTaskText === '') {
this.newTaskError[bucketId] = true this.newTaskError[bucketId] = true
return return
} }
this.newTaskError[bucketId] = false this.newTaskError[bucketId] = false
this.$store.dispatch('tasks/createNewTask', { const task = await this.$store.dispatch('tasks/createNewTask', {
title: this.newTaskText, title: this.newTaskText,
bucketId, bucketId,
listId: this.$route.params.listId, listId: this.$route.params.listId,
}) })
.then(r => { this.newTaskText = ''
this.newTaskText = '' this.$store.commit('kanban/addTaskToBucket', task)
this.$store.commit('kanban/addTaskToBucket', r) this.scrollTaskContainerToBottom(bucketId)
this.scrollTaskContainerToBottom(bucketId)
})
}, },
scrollTaskContainerToBottom(bucketId) { scrollTaskContainerToBottom(bucketId) {
const bucketEl = this.taskContainerRefs[bucketId] const bucketEl = this.taskContainerRefs[bucketId]
if (!bucketEl) { if (!bucketEl) {
@ -461,7 +462,8 @@ export default {
} }
bucketEl.scrollTop = bucketEl.scrollHeight bucketEl.scrollTop = bucketEl.scrollHeight
}, },
createNewBucket() {
async createNewBucket() {
if (this.newBucketTitle === '') { if (this.newBucketTitle === '') {
return return
} }
@ -471,12 +473,11 @@ export default {
listId: parseInt(this.$route.params.listId), listId: parseInt(this.$route.params.listId),
}) })
this.$store.dispatch('kanban/createBucket', newBucket) await this.$store.dispatch('kanban/createBucket', newBucket)
.then(() => { this.newBucketTitle = ''
this.newBucketTitle = '' this.showNewBucketInput = false
this.showNewBucketInput = false
})
}, },
deleteBucketModal(bucketId) { deleteBucketModal(bucketId) {
if (this.buckets.length <= 1) { if (this.buckets.length <= 1) {
return return
@ -485,33 +486,39 @@ export default {
this.bucketToDelete = bucketId this.bucketToDelete = bucketId
this.showBucketDeleteModal = true this.showBucketDeleteModal = true
}, },
deleteBucket() {
this.$store.dispatch('kanban/deleteBucket', {bucket: { async deleteBucket() {
const bucket = new BucketModel({
id: this.bucketToDelete, id: this.bucketToDelete,
listId: parseInt(this.$route.params.listId), listId: parseInt(this.$route.params.listId),
}, params: this.params}) })
.then(() => this.$message.success({message: this.$t('list.kanban.deleteBucketSuccess')}))
.finally(() => { try {
this.showBucketDeleteModal = false await this.$store.dispatch('kanban/deleteBucket', {
bucket,
params: this.params,
}) })
this.$message.success({message: this.$t('list.kanban.deleteBucketSuccess')})
} finally {
this.showBucketDeleteModal = false
}
}, },
focusBucketTitle(e) { focusBucketTitle(e) {
// This little helper allows us to drag a bucket around at the title without focusing on it right away. // This little helper allows us to drag a bucket around at the title without focusing on it right away.
this.bucketTitleEditable = true this.bucketTitleEditable = true
this.$nextTick(() => e.target.focus()) this.$nextTick(() => e.target.focus())
}, },
saveBucketTitle(bucketId, bucketTitle) { async saveBucketTitle(bucketId, bucketTitle) {
const updatedBucketData = { const updatedBucketData = {
id: bucketId, id: bucketId,
title: bucketTitle, title: bucketTitle,
} }
this.$store.dispatch('kanban/updateBucketTitle', updatedBucketData) await this.$store.dispatch('kanban/updateBucketTitle', updatedBucketData)
.then(() => { this.bucketTitleEditable = false
this.bucketTitleEditable = false this.$message.success({message: this.$t('list.kanban.bucketTitleSavedSuccess')})
this.$message.success({message: this.$t('list.kanban.bucketTitleSavedSuccess')})
})
}, },
updateBuckets(value) { updateBuckets(value) {
@ -534,7 +541,8 @@ export default {
this.$store.dispatch('kanban/updateBucket', updatedData) this.$store.dispatch('kanban/updateBucket', updatedData)
}, },
setBucketLimit(bucketId, limit) {
async setBucketLimit(bucketId, limit) {
if (limit < 0) { if (limit < 0) {
return return
} }
@ -544,28 +552,30 @@ export default {
limit, limit,
} }
this.$store.dispatch('kanban/updateBucket', newBucket) await this.$store.dispatch('kanban/updateBucket', newBucket)
.then(() => this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')})) this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')})
}, },
shouldAcceptDrop(bucket) { shouldAcceptDrop(bucket) {
return bucket.id === this.sourceBucket || // When dragging from a bucket who has its limit reached, dragging should still be possible 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.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 bucket.tasks.length < bucket.limit // Disallow dropping to buckets which have their limit reached
}, },
dragstart(bucket) { dragstart(bucket) {
this.drag = true this.drag = true
this.sourceBucket = bucket.id this.sourceBucket = bucket.id
}, },
toggleDoneBucket(bucket) {
async toggleDoneBucket(bucket) {
const newBucket = { const newBucket = {
...bucket, ...bucket,
isDoneBucket: !bucket.isDoneBucket, isDoneBucket: !bucket.isDoneBucket,
} }
this.$store.dispatch('kanban/updateBucket', newBucket) await this.$store.dispatch('kanban/updateBucket', newBucket)
.then(() => this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')})) this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')})
.catch(e => this.$message.error(e))
}, },
collapseBucket(bucket) { collapseBucket(bucket) {
this.collapsedBuckets[bucket.id] = true this.collapsedBuckets[bucket.id] = true
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets) saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)

View file

@ -290,7 +290,8 @@ export default {
} }
sortTasks(this.tasks) sortTasks(this.tasks)
}, },
saveTaskPosition(e) {
async saveTaskPosition(e) {
this.drag = false this.drag = false
const task = this.tasks[e.newIndex] const task = this.tasks[e.newIndex]
@ -302,13 +303,8 @@ export default {
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null), position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
} }
this.$store.dispatch('tasks/update', newTask) const updatedTask = await this.$store.dispatch('tasks/update', newTask)
.then(r => { this.tasks[e.newIndex] = updatedTask
this.tasks[e.newIndex] = r
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -63,23 +63,17 @@ export default {
this.setTitle(this.$t('namespace.create.title')) this.setTitle(this.$t('namespace.create.title'))
}, },
methods: { methods: {
newNamespace() { async newNamespace() {
if (this.namespace.title === '') { if (this.namespace.title === '') {
this.showError = true this.showError = true
return return
} }
this.showError = false this.showError = false
this.namespaceService const namespace = await this.namespaceService.create(this.namespace)
.create(this.namespace) this.$store.commit('namespaces/addNamespace', namespace)
.then((r) => { this.$message.success({message: this.$t('namespace.create.success') })
this.$store.commit('namespaces/addNamespace', r) this.$router.back()
this.$message.success({message: this.$t('namespace.create.success') })
this.$router.back()
})
.catch((e) => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -23,6 +23,7 @@ export default {
title: '', title: '',
} }
}, },
created() { created() {
this.namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id) this.namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.title = this.namespace.isArchived ? this.title = this.namespace.isArchived ?
@ -30,22 +31,18 @@ export default {
this.$t('namespace.archive.titleArchive', { namespace: this.namespace.title }) this.$t('namespace.archive.titleArchive', { namespace: this.namespace.title })
this.setTitle(this.title) this.setTitle(this.title)
}, },
methods: {
archiveNamespace() {
methods: {
async archiveNamespace() {
this.namespace.isArchived = !this.namespace.isArchived this.namespace.isArchived = !this.namespace.isArchived
this.namespaceService.update(this.namespace) try {
.then(r => { const namespace = await this.namespaceService.update(this.namespace)
this.$store.commit('namespaces/setNamespaceById', r) this.$store.commit('namespaces/setNamespaceById', namespace)
this.$message.success({message: this.$t('namespace.archive.success')}) this.$message.success({message: this.$t('namespace.archive.success')})
}) } finally {
.catch(e => { this.$router.back()
this.$message.error(e) }
})
.finally(() => {
this.$router.back()
})
}, },
}, },
} }

View file

@ -35,15 +35,10 @@ export default {
}, },
}, },
methods: { methods: {
deleteNamespace() { async deleteNamespace() {
this.$store.dispatch('namespaces/deleteNamespace', this.namespace) await this.$store.dispatch('namespaces/deleteNamespace', this.namespace)
.then(() => { this.$message.success({message: this.$t('namespace.delete.success')})
this.$message.success({message: this.$t('namespace.delete.success')}) this.$router.push({name: 'home'})
this.$router.push({name: 'home'})
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -27,7 +27,6 @@
<div class="control"> <div class="control">
<editor <editor
:class="{ 'disabled': namespaceService.loading}" :class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
:preview-is-default="false" :preview-is-default="false"
id="namespacedescription" id="namespacedescription"
:placeholder="$t('namespace.attributes.descriptionPlaceholder')" :placeholder="$t('namespace.attributes.descriptionPlaceholder')"
@ -93,8 +92,8 @@ export default {
}, },
}, },
methods: { methods: {
loadNamespace() { async loadNamespace() {
// This makes the editor trigger its mounted function again which makes it forget every input // HACK: This makes the editor trigger its mounted function again which makes it forget every input
// it currently has in its textarea. This is a counter-hack to a hack inside of vue-easymde // it currently has in its textarea. This is a counter-hack to a hack inside of vue-easymde
// which made it impossible to detect change from the outside. Therefore the component would // which made it impossible to detect change from the outside. Therefore the component would
// not update if new content from the outside was made available. // not update if new content from the outside was made available.
@ -103,30 +102,20 @@ export default {
this.$nextTick(() => this.editorActive = true) this.$nextTick(() => this.editorActive = true)
const namespace = new NamespaceModel({id: this.$route.params.id}) const namespace = new NamespaceModel({id: this.$route.params.id})
this.namespaceService.get(namespace) this.namespace = await this.namespaceService.get(namespace)
.then(r => { // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.namespace = r this.manageTeamsComponent = 'manageSharing'
// This will trigger the dynamic loading of components once we actually have all the data to pass to them this.manageUsersComponent = 'manageSharing'
this.manageTeamsComponent = 'manageSharing' this.title = this.$t('namespace.edit.title', {namespace: this.namespace.title})
this.manageUsersComponent = 'manageSharing' this.setTitle(this.title)
this.title = this.$t('namespace.edit.title', {namespace: r.title})
this.setTitle(this.title)
})
.catch(e => {
this.$message.error(e)
})
}, },
save() {
this.namespaceService.update(this.namespace) async save() {
.then(r => { const namespace = await this.namespaceService.update(this.namespace)
// Update the namespace in the parent // Update the namespace in the parent
this.$store.commit('namespaces/setNamespaceById', r) this.$store.commit('namespaces/setNamespaceById', namespace)
this.$message.success({message: this.$t('namespace.edit.success')}) this.$message.success({message: this.$t('namespace.edit.success')})
this.$router.back() this.$router.back()
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -57,20 +57,14 @@ export default {
}, },
}, },
methods: { methods: {
loadNamespace() { async loadNamespace() {
const namespace = new NamespaceModel({id: this.$route.params.id}) const namespace = new NamespaceModel({id: this.$route.params.id})
this.namespaceService.get(namespace) this.namespace = await this.namespaceService.get(namespace)
.then(r => { // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.namespace = r this.manageTeamsComponent = 'manageSharing'
// This will trigger the dynamic loading of components once we actually have all the data to pass to them this.manageUsersComponent = 'manageSharing'
this.manageTeamsComponent = 'manageSharing' this.title = this.$t('namespace.share.title', { namespace: this.namespace.title })
this.manageUsersComponent = 'manageSharing' this.setTitle(this.title)
this.title = this.$t('namespace.share.title', { namespace: this.namespace.title })
this.setTitle(this.title)
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -57,7 +57,7 @@ export default {
'authLinkShare', 'authLinkShare',
]), ]),
methods: { methods: {
auth() { async auth() {
this.errorMessage = '' this.errorMessage = ''
if (this.authLinkShare) { if (this.authLinkShare) {
@ -66,29 +66,30 @@ export default {
this.loading = true this.loading = true
this.$store.dispatch('auth/linkShareAuth', {hash: this.$route.params.share, password: this.password}) try {
.then((r) => { const r = await this.$store.dispatch('auth/linkShareAuth', {
this.$router.push({name: 'list.list', params: {listId: r.list_id}}) hash: this.$route.params.share,
password: this.password,
}) })
.catch(e => { this.$router.push({name: 'list.list', params: {listId: r.list_id}})
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) { } catch(e) {
this.authenticateWithPassword = true if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
return this.authenticateWithPassword = true
} return
}
// TODO: Put this logic in a global errorMessage handler method which checks all auth codes // TODO: Put this logic in a global errorMessage handler method which checks all auth codes
let errorMessage = this.$t('sharing.error') let errorMessage = this.$t('sharing.error')
if (e.response && e.response.data && e.response.data.message) { if (e.response && e.response.data && e.response.data.message) {
errorMessage = e.response.data.message errorMessage = e.response.data.message
} }
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13002) { if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13002) {
errorMessage = this.$t('sharing.invalidPassword') errorMessage = this.$t('sharing.invalidPassword')
} }
this.errorMessage = errorMessage this.errorMessage = errorMessage
}) } finally {
.finally(() => { this.loading = false
this.loading = false }
})
}, },
}, },
} }

View file

@ -33,15 +33,12 @@
</h3> </h3>
<div v-if="!showAll" class="mb-4"> <div v-if="!showAll" class="mb-4">
<x-button type="secondary" @click="showTodaysTasks()" class="mr-2">{{ $t('task.show.today') }}</x-button> <x-button type="secondary" @click="showTodaysTasks()" class="mr-2">{{ $t('task.show.today') }}</x-button>
<x-button type="secondary" @click="setDatesToNextWeek()" class="mr-2">{{ <x-button type="secondary" @click="setDatesToNextWeek()" class="mr-2">{{ $t('task.show.nextWeek') }}</x-button>
$t('task.show.nextWeek')
}}
</x-button>
<x-button type="secondary" @click="setDatesToNextMonth()">{{ $t('task.show.nextMonth') }}</x-button> <x-button type="secondary" @click="setDatesToNextMonth()">{{ $t('task.show.nextMonth') }}</x-button>
</div> </div>
<template v-if="!loading && (!tasks || tasks.length === 0) && showNothingToDo"> <template v-if="!loading && (!tasks || tasks.length === 0) && showNothingToDo">
<h3 class="nothing">{{ $t('task.show.noTasks') }}</h3> <h3 class="nothing">{{ $t('task.show.noTasks') }}</h3>
<img alt="" :src="llamaCoolUrl"/> <img alt="" :src="llamaCoolUrl" />
</template> </template>
<div :class="{ 'is-loading': loading}" class="spinner"></div> <div :class="{ 'is-loading': loading}" class="spinner"></div>
@ -144,7 +141,7 @@ export default {
}, },
}) })
}, },
loadPendingTasks() { async loadPendingTasks() {
// Since this route is authentication only, users would get an error message if they access the page unauthenticated. // Since this route is authentication only, users would get an error message if they access the page unauthenticated.
// Since this component is mounted as the home page before unauthenticated users get redirected // Since this component is mounted as the home page before unauthenticated users get redirected
// to the login page, they will almost always see the error message. // to the login page, they will almost always see the error message.
@ -203,27 +200,22 @@ export default {
} }
} }
this.$store.dispatch('tasks/loadTasks', params) const tasks = await this.$store.dispatch('tasks/loadTasks', params)
.then(r => {
// Sorting tasks with a due date so that the soonest or overdue are displayed at the top of the list. // FIXME: sort tasks in computed
const tasksWithDueDates = r // Sort all tasks to put those with a due date before the ones without a due date, the
.filter(t => t.dueDate !== null) // soonest before the later ones.
.sort((a, b) => a.dueDate > b.dueDate ? 1 : -1) // We can't use the api sorting here because that sorts tasks with a due date after
// ones without a due date.
const tasksWithoutDueDates = r.filter(t => t.dueDate === null) this.tasks = tasks.sort((a, b) => {
const sortByDueDate = b.dueDate - a.dueDate
const tasks = [ return sortByDueDate === 0
...tasksWithDueDates, ? b.id - a.id
...tasksWithoutDueDates, : sortByDueDate
] })
this.tasks = tasks
})
.catch(e => {
this.$message.error(e)
})
}, },
// FIXME: this modification should happen in the store
updateTasks(updatedTask) { updateTasks(updatedTask) {
for (const t in this.tasks) { for (const t in this.tasks) {
if (this.tasks[t].id === updatedTask.id) { if (this.tasks[t].id === updatedTask.id) {
@ -237,18 +229,21 @@ export default {
} }
} }
}, },
setDatesToNextWeek() { setDatesToNextWeek() {
this.cStartDate = new Date() this.cStartDate = new Date()
this.cEndDate = new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000) this.cEndDate = new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
this.showOverdue = false this.showOverdue = false
this.setDate() this.setDate()
}, },
setDatesToNextMonth() { setDatesToNextMonth() {
this.cStartDate = new Date() this.cStartDate = new Date()
this.cEndDate = new Date((new Date()).setMonth((new Date()).getMonth() + 1)) this.cEndDate = new Date((new Date()).setMonth((new Date()).getMonth() + 1))
this.showOverdue = false this.showOverdue = false
this.setDate() this.setDate()
}, },
showTodaysTasks() { showTodaysTasks() {
const d = new Date() const d = new Date()
this.cStartDate = new Date() this.cStartDate = new Date()

View file

@ -564,26 +564,22 @@ export default {
return uploadFile(this.taskId, ...args) return uploadFile(this.taskId, ...args)
}, },
loadTask(taskId) { async loadTask(taskId) {
if (taskId === undefined) { if (taskId === undefined) {
return return
} }
this.taskService.get({id: taskId}) try {
.then(r => { this.task = await this.taskService.get({id: taskId})
this.task = r this.$store.commit('attachments/set', this.task.attachments)
this.$store.commit('attachments/set', r.attachments) this.taskColor = this.task.hexColor
this.taskColor = this.task.hexColor this.setActiveFields()
this.setActiveFields() this.setTitle(this.task.title)
this.setTitle(this.task.title) } finally {
}) this.scrollToHeading()
.catch(e => { await this.$nextTick()
this.$message.error(e) this.visible = true
}) }
.finally(() => {
this.$nextTick(() => this.visible = true)
this.scrollToHeading()
})
}, },
scrollToHeading() { scrollToHeading() {
this.$refs.heading.$el.scrollIntoView({block: 'center'}) this.$refs.heading.$el.scrollIntoView({block: 'center'})
@ -623,26 +619,23 @@ export default {
this.task.endDate = this.task.dueDate this.task.endDate = this.task.dueDate
} }
try { this.task = await this.$store.dispatch('tasks/update', this.task)
this.task = await this.$store.dispatch('tasks/update', this.task) this.setActiveFields()
this.setActiveFields()
if (!showNotification) { if (!showNotification) {
return return
}
let actions = []
if (undoCallback !== null) {
actions = [{
title: 'Undo',
callback: undoCallback,
}]
}
this.$message.success({message: this.$t('task.detail.updateSuccess')}, actions)
} catch(e) {
this.$message.error(e)
} }
let actions = []
if (undoCallback !== null) {
actions = [{
title: 'Undo',
callback: undoCallback,
}]
}
this.$message.success({message: this.$t('task.detail.updateSuccess')}, actions)
}, },
setFieldActive(fieldName) { setFieldActive(fieldName) {
this.activeFields[fieldName] = true this.activeFields[fieldName] = true
this.$nextTick(() => { this.$nextTick(() => {
@ -661,16 +654,13 @@ export default {
} }
}) })
}, },
deleteTask() {
this.$store.dispatch('tasks/delete', this.task) async deleteTask() {
.then(() => { await this.$store.dispatch('tasks/delete', this.task)
this.$message.success({message: this.$t('task.detail.deleteSuccess')}) this.$message.success({message: this.$t('task.detail.deleteSuccess')})
this.$router.push({name: 'list.index', params: {listId: this.task.listId}}) this.$router.push({name: 'list.index', params: {listId: this.task.listId}})
})
.catch(e => {
this.$message.error(e)
})
}, },
toggleTaskDone() { toggleTaskDone() {
this.task.done = !this.task.done this.task.done = !this.task.done
@ -678,39 +668,26 @@ export default {
playPop() playPop()
} }
this.saveTask(true, () => this.toggleTaskDone()) this.saveTask(true, this.toggleTaskDone)
}, },
setDescriptionChanged(e) { setDescriptionChanged(e) {
if (e.key === 'Enter' || e.key === 'Control') { if (e.key === 'Enter' || e.key === 'Control') {
return return
} }
this.descriptionChanged = true this.descriptionChanged = true
}, },
saveTaskIfDescriptionChanged() {
// We want to only save the description if it was changed.
// Since we can either trigger this with ctrl+enter or @change, it would be possible to save a task first
// with ctrl+enter and then with @change although nothing changed since the last save when @change gets fired.
// To only save one time we added this method.
if (this.descriptionChanged) {
this.descriptionChanged = false
this.saveTask()
}
},
async changeList(list) { async changeList(list) {
this.$store.commit('kanban/removeTaskInBucket', this.task) this.$store.commit('kanban/removeTaskInBucket', this.task)
this.task.listId = list.id this.task.listId = list.id
await this.saveTask() await this.saveTask()
}, },
toggleFavorite() {
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite this.task.isFavorite = !this.task.isFavorite
this.taskService.update(this.task) this.task = await this.taskService.update(this.task)
.then(t => { this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
this.task = t
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -219,114 +219,79 @@ export default {
userInfo: (state) => state.auth.info, userInfo: (state) => state.auth.info,
}), }),
}, },
methods: { methods: {
loadTeam() { async loadTeam() {
this.team = new TeamModel({id: this.teamId}) this.team = new TeamModel({id: this.teamId})
this.teamService this.team = await this.teamService.get(this.team)
.get(this.team) this.title = this.$t('team.edit.title', {team: this.team.name})
.then((response) => { this.setTitle(this.title)
this.team = response
this.title = this.$t('team.edit.title', {team: this.team.name})
this.setTitle(this.title)
})
.catch((e) => {
this.$message.error(e)
})
}, },
save() {
async save() {
if (this.team.name === '') { if (this.team.name === '') {
this.showError = true this.showError = true
return return
} }
this.showError = false this.showError = false
this.teamService this.team = await this.teamService.update(this.team)
.update(this.team) this.$message.success({message: this.$t('team.edit.success')})
.then((response) => {
this.team = response
this.$message.success({message: this.$t('team.edit.success')})
})
.catch((e) => {
this.$message.error(e)
})
}, },
deleteTeam() {
this.teamService async deleteTeam() {
.delete(this.team) await this.teamService.delete(this.team)
.then(() => { this.$message.success({message: this.$t('team.edit.delete.success')})
this.$message.success({message: this.$t('team.edit.delete.success')}) this.$router.push({name: 'teams.index'})
this.$router.push({name: 'teams.index'})
})
.catch((e) => {
this.$message.error(e)
})
}, },
deleteUser() {
this.teamMemberService async deleteUser() {
.delete(this.member) try {
.then(() => { await this.teamMemberService.delete(this.member)
this.$message.success({message: this.$t('team.edit.deleteUser.success')}) this.$message.success({message: this.$t('team.edit.deleteUser.success')})
this.loadTeam() this.loadTeam()
}) } finally {
.catch((e) => { this.showUserDeleteModal = false
this.$message.error(e) }
})
.finally(() => {
this.showUserDeleteModal = false
})
}, },
addUser() {
async addUser() {
const newMember = new TeamMemberModel({ const newMember = new TeamMemberModel({
teamId: this.teamId, teamId: this.teamId,
username: this.newMember.username, username: this.newMember.username,
}) })
this.teamMemberService await this.teamMemberService.create(newMember)
.create(newMember) this.loadTeam()
.then(() => { this.$message.success({message: this.$t('team.edit.userAddedSuccess')})
this.loadTeam()
this.$message.success({message: this.$t('team.edit.userAddedSuccess')})
})
.catch((e) => {
this.$message.error(e)
})
}, },
toggleUserType(member) {
async toggleUserType(member) {
// FIXME: direct manipulation
member.admin = !member.admin member.admin = !member.admin
member.teamId = this.teamId member.teamId = this.teamId
this.teamMemberService const r = await this.teamMemberService.update(member)
.update(member) for (const tm in this.team.members) {
.then((r) => { if (this.team.members[tm].id === member.id) {
for (const tm in this.team.members) { this.team.members[tm].admin = r.admin
if (this.team.members[tm].id === member.id) { break
this.team.members[tm].admin = r.admin }
break }
} this.$message.success({
} message: member.admin ?
this.$message.success({ this.$t('team.edit.madeAdmin') :
message: member.admin ? this.$t('team.edit.madeMember'),
this.$t('team.edit.madeAdmin') : })
this.$t('team.edit.madeMember'),
})
})
.catch((e) => {
this.$message.error(e)
})
}, },
findUser(query) {
async findUser(query) {
if (query === '') { if (query === '') {
this.clearAll() this.clearAll()
return return
} }
this.userService this.foundUsers = await this.userService.getAll({}, {s: query})
.getAll({}, {s: query})
.then((response) => {
this.foundUsers = response
})
.catch((e) => {
this.$message.error(e)
})
}, },
clearAll() { clearAll() {
this.foundUsers = [] this.foundUsers = []
}, },

View file

@ -43,14 +43,8 @@ export default {
this.setTitle(this.$t('team.title')) this.setTitle(this.$t('team.title'))
}, },
methods: { methods: {
loadTeams() { async loadTeams() {
this.teamService.getAll() this.teams = await this.teamService.getAll()
.then(response => {
this.teams = response
})
.catch(e => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -49,25 +49,19 @@ export default {
this.setTitle(this.$t('team.create.title')) this.setTitle(this.$t('team.create.title'))
}, },
methods: { methods: {
newTeam() { async newTeam() {
if (this.team.name === '') { if (this.team.name === '') {
this.showError = true this.showError = true
return return
} }
this.showError = false this.showError = false
this.teamService const response = await this.teamService.create(this.team)
.create(this.team) this.$router.push({
.then((response) => { name: 'teams.edit',
this.$router.push({ params: { id: response.id },
name: 'teams.edit', })
params: { id: response.id }, this.$message.success({message: this.$t('team.create.success') })
})
this.$message.success({message: this.$t('team.create.success') })
})
.catch((e) => {
this.$message.error(e)
})
}, },
}, },
} }

View file

@ -57,7 +57,6 @@ export default {
} }
this.dataExportService.download(this.password) this.dataExportService.download(this.password)
.catch(e => this.$message.error(e))
}, },
}, },
} }

View file

@ -187,7 +187,8 @@ export default {
this.loading = false this.loading = false
} }
}, },
submit() {
async submit() {
this.$store.commit(ERROR_MESSAGE, '') this.$store.commit(ERROR_MESSAGE, '')
// Some browsers prevent Vue bindings from working with autofilled values. // Some browsers prevent Vue bindings from working with autofilled values.
// To work around this, we're manually getting the values here instead of relying on vue bindings. // To work around this, we're manually getting the values here instead of relying on vue bindings.
@ -201,24 +202,24 @@ export default {
credentials.totpPasscode = this.$refs.totpPasscode.value credentials.totpPasscode = this.$refs.totpPasscode.value
} }
this.$store.dispatch('auth/login', credentials) try {
.then(() => { await this.$store.dispatch('auth/login', credentials)
this.$store.commit('auth/needsTotpPasscode', false) this.$store.commit('auth/needsTotpPasscode', false)
}) } catch(e) {
.catch(e => { if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) {
if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) { return
return }
}
const err = getErrorText(e) const err = getErrorText(e)
if (typeof err[1] !== 'undefined') { if (typeof err[1] !== 'undefined') {
this.$store.commit(ERROR_MESSAGE, err[1]) this.$store.commit(ERROR_MESSAGE, err[1])
return return
} }
this.$store.commit(ERROR_MESSAGE, err[0]) this.$store.commit(ERROR_MESSAGE, err[0])
}) }
}, },
redirectToProvider(provider) { redirectToProvider(provider) {
redirectToProvider(provider, this.openidConnect.redirectUrl) redirectToProvider(provider, this.openidConnect.redirectUrl)
}, },

View file

@ -26,7 +26,7 @@ export default {
this.authenticateWithCode() this.authenticateWithCode()
}, },
methods: { methods: {
authenticateWithCode() { async authenticateWithCode() {
// This component gets mounted twice: The first time when the actual auth request hits the frontend, // This component gets mounted twice: The first time when the actual auth request hits the frontend,
// the second time after that auth request succeeded and the outer component "content-no-auth" isn't used // the second time after that auth request succeeded and the outer component "content-no-auth" isn't used
// but instead the "content-auth" component is used. Because this component is just a route and thus // but instead the "content-auth" component is used. Because this component is just a route and thus
@ -59,34 +59,32 @@ export default {
this.$store.commit(ERROR_MESSAGE, '') this.$store.commit(ERROR_MESSAGE, '')
this.$store.dispatch('auth/openIdAuth', { try {
provider: this.$route.params.provider, await this.$store.dispatch('auth/openIdAuth', {
code: this.$route.query.code, provider: this.$route.params.provider,
}) code: this.$route.query.code,
.then(() => {
const last = getLastVisited()
if (last !== null) {
this.$router.push({
name: last.name,
params: last.params,
})
clearLastVisited()
} else {
this.$router.push({name: 'home'})
}
}) })
.catch(e => { const last = getLastVisited()
const err = getErrorText(e) if (last !== null) {
if (typeof err[1] !== 'undefined') { this.$router.push({
this.$store.commit(ERROR_MESSAGE, err[1]) name: last.name,
return params: last.params,
} })
clearLastVisited()
} else {
this.$router.push({name: 'home'})
}
} catch(e) {
const err = getErrorText(e)
if (typeof err[1] !== 'undefined') {
this.$store.commit(ERROR_MESSAGE, err[1])
return
}
this.$store.commit(ERROR_MESSAGE, err[0]) this.$store.commit(ERROR_MESSAGE, err[0])
}) } finally {
.finally(() => { localStorage.removeItem('authenticating')
localStorage.removeItem('authenticating') }
})
}, },
}, },
} }

View file

@ -85,11 +85,13 @@ export default {
successMessage: '', successMessage: '',
} }
}, },
mounted() { mounted() {
this.setTitle(this.$t('user.auth.resetPassword')) this.setTitle(this.$t('user.auth.resetPassword'))
}, },
methods: { methods: {
submit() { async submit() {
this.errorMsg = '' this.errorMsg = ''
if (this.credentials.password2 !== this.credentials.password) { if (this.credentials.password2 !== this.credentials.password) {
@ -98,14 +100,13 @@ export default {
} }
let passwordReset = new PasswordResetModel({newPassword: this.credentials.password}) let passwordReset = new PasswordResetModel({newPassword: this.credentials.password})
this.passwordResetService.resetPassword(passwordReset) try {
.then(response => { const { message } = this.passwordResetService.resetPassword(passwordReset)
this.successMessage = response.message this.successMessage = message
localStorage.removeItem('passwordResetToken') localStorage.removeItem('passwordResetToken')
}) } catch(e) {
.catch(e => { this.errorMsg = e.response.data.message
this.errorMsg = e.response.data.message }
})
}, },
}, },
} }

View file

@ -147,7 +147,6 @@ export default {
} }
this.$store.dispatch('auth/register', credentials) this.$store.dispatch('auth/register', credentials)
.catch(() => {})
}, },
}, },
} }

View file

@ -69,15 +69,14 @@ export default {
this.setTitle(this.$t('user.auth.resetPassword')) this.setTitle(this.$t('user.auth.resetPassword'))
}, },
methods: { methods: {
submit() { async submit() {
this.errorMsg = '' this.errorMsg = ''
this.passwordResetService.requestResetPassword(this.passwordReset) try {
.then(() => { await this.passwordResetService.requestResetPassword(this.passwordReset)
this.isSuccess = true this.isSuccess = true
}) } catch(e) {
.catch(e => { this.errorMsg = e.response.data.message
this.errorMsg = e.response.data.message }
})
}, },
}, },
} }

View file

@ -385,90 +385,75 @@ export default {
methods: { methods: {
copy, copy,
updatePassword() { async updatePassword() {
if (this.passwordConfirm !== this.passwordUpdate.newPassword) { if (this.passwordConfirm !== this.passwordUpdate.newPassword) {
this.$message.error({message: this.$t('user.settings.passwordsDontMatch')}) this.$message.error({message: this.$t('user.settings.passwordsDontMatch')})
return return
} }
this.passwordUpdateService.update(this.passwordUpdate) await this.passwordUpdateService.update(this.passwordUpdate)
.then(() => { this.$message.success({message: this.$t('user.settings.passwordUpdateSuccess')})
this.$message.success({message: this.$t('user.settings.passwordUpdateSuccess')})
})
.catch(e => this.$message.error(e))
}, },
updateEmail() {
this.emailUpdateService.update(this.emailUpdate) async updateEmail() {
.then(() => { await this.emailUpdateService.update(this.emailUpdate)
this.$message.success({message: this.$t('user.settings.updateEmailSuccess')}) this.$message.success({message: this.$t('user.settings.updateEmailSuccess')})
})
.catch(e => this.$message.error(e))
}, },
totpStatus() {
async totpStatus() {
if (!this.totpEnabled) { if (!this.totpEnabled) {
return return
} }
this.totpService.get() try {
.then(r => { this.totp = await this.totpService.get()
this.totp = r this.totpSetQrCode()
this.totpSetQrCode() } catch(e) {
}) // Error code 1016 means totp is not enabled, we don't need an error in that case.
.catch(e => { if (e.response && e.response.data && e.response.data.code && e.response.data.code === 1016) {
// Error code 1016 means totp is not enabled, we don't need an error in that case.
if (e.response && e.response.data && e.response.data.code && e.response.data.code === 1016) {
this.totpEnrolled = false
return
}
this.$message.error(e)
})
},
totpSetQrCode() {
this.totpService.qrcode()
.then(qr => {
const urlCreator = window.URL || window.webkitURL
this.totpQR = urlCreator.createObjectURL(qr)
})
},
totpEnroll() {
this.totpService.enroll()
.then(r => {
this.totpEnrolled = true
this.totp = r
this.totpSetQrCode()
})
.catch(e => this.$message.error(e))
},
totpConfirm() {
this.totpService.enable({passcode: this.totpConfirmPasscode})
.then(() => {
this.totp.enabled = true
this.$message.success({message: this.$t('user.settings.totp.confirmSuccess')})
})
.catch(e => this.$message.error(e))
},
totpDisable() {
this.totpService.disable({password: this.totpDisablePassword})
.then(() => {
this.totpEnrolled = false this.totpEnrolled = false
this.totp = new TotpModel() return
this.$message.success({message: this.$t('user.settings.totp.disableSuccess')}) }
})
.catch(e => this.$message.error(e)) throw e
}
}, },
updateSettings() {
async totpSetQrCode() {
const qr = await this.totpService.qrcode()
const urlCreator = window.URL || window.webkitURL
this.totpQR = urlCreator.createObjectURL(qr)
},
async totpEnroll() {
this.totp = await this.totpService.enroll()
this.totpEnrolled = true
this.totpSetQrCode()
},
async totpConfirm() {
await this.totpService.enable({passcode: this.totpConfirmPasscode})
this.totp.enabled = true
this.$message.success({message: this.$t('user.settings.totp.confirmSuccess')})
},
async totpDisable() {
await this.totpService.disable({password: this.totpDisablePassword})
this.totpEnrolled = false
this.totp = new TotpModel()
this.$message.success({message: this.$t('user.settings.totp.disableSuccess')})
},
async updateSettings() {
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone) localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
saveLanguage(this.language) saveLanguage(this.language)
setQuickAddMagicMode(this.quickAddMagicMode) setQuickAddMagicMode(this.quickAddMagicMode)
this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0 this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0
this.userSettingsService.update(this.settings) await this.userSettingsService.update(this.settings)
.then(() => { this.$store.commit('auth/setUserSettings', this.settings)
this.$store.commit('auth/setUserSettings', this.settings) this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
})
.catch(e => this.$message.error(e))
}, },
anchorHashCheck() { anchorHashCheck() {
if (window.location.hash === this.$route.hash) { if (window.location.hash === this.$route.hash) {
const el = document.getElementById(this.$route.hash.slice(1)) const el = document.getElementById(this.$route.hash.slice(1))