feat: use async / await where it makes sense

This commit is contained in:
Dominik Pschenitschni 2021-10-11 19:37:20 +02:00
parent a776e1d2f3
commit bb94c1ba3a
No known key found for this signature in database
GPG key ID: B257AC0149F43A77
74 changed files with 1458 additions and 1662 deletions

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,28 +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) localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
this.$router.push({name: 'user.password-reset.reset'}) 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) localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
this.$router.push({name: 'user.login'}) this.$router.push({name: 'user.login'})
}
}, },
setupAccountDeletionVerification() { async setupAccountDeletionVerification() {
if (typeof this.$route.query.accountDeletionConfirm !== 'undefined') { if (typeof this.$route.query.accountDeletionConfirm === 'undefined') {
return
}
const accountDeletionService = new AccountDeleteService() const accountDeletionService = new AccountDeleteService()
accountDeletionService.confirm(this.$route.query.accountDeletionConfirm) await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
.then(() => {
this.$message.success({message: this.$t('user.deletion.confirmSuccess')}) this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
this.$store.dispatch('auth/refreshUserInfo') this.$store.dispatch('auth/refreshUserInfo')
})
}
}, },
}, },
}) })

View file

@ -195,6 +195,7 @@ export default {
}, },
}, },
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 => {
@ -248,7 +249,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
@ -257,14 +259,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)
try {
// create a copy of the list in order to not violate vuex mutations // create a copy of the list in order to not violate vuex mutations
this.$store.dispatch('lists/updateList', { await this.$store.dispatch('lists/updateList', {
...list, ...list,
position, position,
}) })
.finally(() => { } finally {
this.listUpdating[list.id] = false 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,12 +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
})
} }
this[kind] = await this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]})
}, },
setDoneFilter() { setDoneFilter() {
if (this.filters.done) { if (this.filters.done) {
@ -523,17 +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))
})
}, },
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,11 +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
.finally(() => this.backgroundLoading = false) }
}, },
toggleFavoriteList(list) { toggleFavoriteList(list) {
// The favorites pseudo list is always favorite // The favorites pseudo list is always favorite

View file

@ -141,43 +141,32 @@ export default {
} }
}, },
methods: { methods: {
getAuthUrl() { async getAuthUrl() {
this.migrationService.getAuthUrl() const { url } = this.migrationService.getAuthUrl()
.then(r => { this.authUrl = url
this.authUrl = r.url
})
}, },
migrate() {
async migrate() {
this.isMigrating = true this.isMigrating = true
this.lastMigrationDate = null this.lastMigrationDate = null
this.message = '' this.message = ''
if (this.isFileMigrator) { let migrationConfig = { code: this.migratorAuthCode }
return this.migrateFile()
}
this.migrationService.migrate({code: this.migratorAuthCode}) if (this.isFileMigrator) {
.then(r => {
this.message = r.message
this.$store.dispatch('namespaces/loadNamespaces')
})
.finally(() => {
this.isMigrating = false
})
},
migrateFile() {
if (this.$refs.uploadInput.files.length === 0) { if (this.$refs.uploadInput.files.length === 0) {
return return
} }
migrationConfig = this.$refs.uploadInput.files[0]
}
this.migrationService.migrate(this.$refs.uploadInput.files[0]) 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 {
.finally(() => {
this.isMigrating = false this.isMigrating = false
}) }
}, },
}, },
} }

View file

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

View file

@ -93,11 +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
})
}, },
to(n, index) { to(n, index) {
const to = { const to = {
@ -124,16 +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
})
} }
}, },
}, },

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) === null ? null : this.$store.getters['lists/getListById'](t.listId) const list = 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,22 +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: r.id}}) this.$router.push({name: 'task.detail', params: {id: task.id}})
this.closeQuickActions() this.closeQuickActions()
})
}, },
newList() { async newList() {
if (this.currentList === null) { if (this.currentList === null) {
return return
} }
@ -402,33 +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: r.id}}) this.$router.push({name: 'list.index', params: {listId: list.id}})
this.closeQuickActions() this.closeQuickActions()
})
}, },
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()
})
}, },
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: r.id}, params: {id: team.id},
}) })
this.$message.success({message: this.$t('team.create.success')}) this.$message.success({message: this.$t('team.create.success')})
this.closeQuickActions() this.closeQuickActions()
})
}, },
select(parentIndex, index) { select(parentIndex, index) {

View file

@ -215,50 +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
})
}, },
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)
.then(() => {
this.selectedRight = rights.READ this.selectedRight = rights.READ
this.name = '' this.name = ''
this.password = '' this.password = ''
this.showNewForm = false this.showNewForm = false
this.$message.success({message: this.$t('list.share.links.createSuccess')}) this.$message.success({message: this.$t('list.share.links.createSuccess')})
this.load(listId) await this.load(listId)
})
}, },
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')})
this.load(listId) await this.load(listId)
}) } finally {
.finally(() => {
this.showDeleteModal = false this.showDeleteModal = false
}) }
}, },
copy, copy,
getShareLink(hash) { getShareLink(hash) {

View file

@ -272,26 +272,21 @@ 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.sharables = r
r.forEach((s) =>
this.selectedRight[s.id] = s.right, this.selectedRight[s.id] = s.right,
) )
})
}, },
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)
.then(() => {
this.showDeleteModal = false this.showDeleteModal = false
for (const i in this.sharables) { for (const i in this.sharables) {
if ( if (
@ -302,9 +297,9 @@ export default {
} }
} }
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})})
})
}, },
add(admin) {
async add(admin) {
if (admin === null) { if (admin === null) {
admin = false admin = false
} }
@ -319,14 +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)
.then(() => {
this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})}) this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})})
this.load() await this.load()
})
}, },
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 &&
@ -342,9 +335,7 @@ 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)
.then((r) => {
for (const i in this.sharables) { for (const i in this.sharables) {
if ( if (
(this.sharables[i].username === (this.sharables[i].username ===
@ -357,20 +348,17 @@ export default {
} }
} }
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})})
})
}, },
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
})
}, },
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,
}) })
.then(task => {
this.$emit('taskAdded', task) this.$emit('taskAdded', task)
return task return task
}),
)
}) })
return Promise.all(newTasks) try {
.then(() => { await Promise.all(newTasks)
this.newTaskTitle = '' this.newTaskTitle = ''
}) } catch(e) {
.catch(e => {
if (e.message === 'NO_LIST') { if (e.message === 'NO_LIST') {
this.errorMessage = this.$t('list.create.addListRequired') this.errorMessage = this.$t('list.create.addListRequired')
return return
} }
throw e throw 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,14 +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)
.then((r) => {
this.taskEditTask = r
this.initTaskFields() this.initTaskFields()
this.$message.success({message: this.$t('task.detail.updateSuccess')}) this.$message.success({message: this.$t('task.detail.updateSuccess')})
})
}, },
}, },
} }

View file

@ -297,30 +297,26 @@ 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)
.then((tasks) => {
if (page < this.taskCollectionService.totalPages) { if (page < this.taskCollectionService.totalPages) {
return getAllTasks(page + 1).then((nextTasks) => { const nextTasks = await getAllTasks(page + 1)
return tasks.concat(nextTasks) return tasks.concat(nextTasks)
}) }
} else {
return tasks return tasks
} }
})
}
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) {
@ -331,15 +327,12 @@ export default {
t.endDate <= this.endDate t.endDate <= this.endDate
) )
}) })
.map((t) => { .map((t) => this.addGantAttributes(t))
return this.addGantAttributes(t)
})
.sort(function (a, b) { .sort(function (a, b) {
if (a.startDate < b.startDate) return -1 if (a.startDate < b.startDate) return -1
if (a.startDate > b.startDate) return 1 if (a.startDate > b.startDate) return 1
return 0 return 0
}) })
})
}, },
addGantAttributes(t) { addGantAttributes(t) {
if (typeof t.durationDays !== 'undefined' && typeof t.offsetDays !== 'undefined') { if (typeof t.durationDays !== 'undefined' && typeof t.offsetDays !== 'undefined') {
@ -351,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
} }
@ -392,9 +385,7 @@ export default {
offsetDays: newTask.offsetDays, offsetDays: newTask.offsetDays,
} }
this.taskService const r = await this.taskService.update(newTask)
.update(newTask)
.then(r => {
r.endDate = ganttData.endDate r.endDate = ganttData.endDate
r.durationDays = ganttData.durationDays r.durationDays = ganttData.durationDays
r.offsetDays = ganttData.offsetDays r.offsetDays = ganttData.offsetDays
@ -416,7 +407,6 @@ export default {
} }
} }
} }
})
}, },
editTask(task) { editTask(task) {
this.taskToEdit = task this.taskToEdit = task
@ -436,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
} }
@ -444,13 +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)
.then((r) => {
this.tasksWithoutDates.push(this.addGantAttributes(r)) this.tasksWithoutDates.push(this.addGantAttributes(r))
this.newTaskTitle = '' this.newTaskTitle = ''
this.hideCrateNewTask() this.hideCrateNewTask()
})
}, },
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,14 +76,9 @@ export default {
} }
this.tasks = [] this.tasks = []
this.tasks = await this.taskCollectionService.getAll(list, params, page)
this.taskCollectionService.getAll(list, params, page)
.then(r => {
this.tasks = r
this.currentPage = page this.currentPage = page
this.loadedList = JSON.parse(JSON.stringify(currentList)) this.loadedList = JSON.parse(JSON.stringify(currentList))
})
}, },
loadTasksForPage(e) { loadTasksForPage(e) {

View file

@ -218,21 +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{
.finally(() => {
this.showDeleteModal = false 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') ||
@ -240,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,40 +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: {
computed: mapState({ ...mapState({
userAvatar: state => state.auth.info.getAvatarUrl(48), userAvatar: state => state.auth.info.getAvatarUrl(48),
enabled: state => state.config.taskCommentsEnabled, 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'),
}],
])))
},
},
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
this.commentToDelete.taskId = taskId
this.comments = await this.taskCommentService.getAll({taskId})
}, },
addComment() {
async addComment() {
if (this.newComment.comment === '') { if (this.newComment.comment === '') {
return return
} }
@ -242,27 +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')})
this.makeActions() } finally {
})
.finally(() => {
this.creating = false 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
} }
@ -270,48 +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] = r this.comments[c] = comment
} }
} }
this.saved = this.commentEdit.id this.saved = this.commentEdit.id
setTimeout(() => { setTimeout(() => {
this.saved = null this.saved = null
}, 2000) }, 2000)
}) } finally {
.finally(() => {
this.isCommentEdit = false this.isCommentEdit = false
this.saving = null 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 {
}
}
})
.finally(() => {
this.showDeleteModal = false 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,13 +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)
})
}, },
}, },
} }

View file

@ -71,21 +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 {
.finally(() => {
this.saving = false this.saving = false
}) }
}, },
}, },
} }

View file

@ -78,16 +78,15 @@ 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')})
})
}, },
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 // Remove the assignee from the list
for (const a in this.assignees) { for (const a in this.assignees) {
if (this.assignees[a].id === user.id) { if (this.assignees[a].id === user.id) {
@ -95,23 +94,24 @@ export default {
} }
} }
this.$message.success({message: this.$t('task.assignee.unassignSuccess')}) this.$message.success({message: this.$t('task.assignee.unassignSuccess')})
})
}, },
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))
})
}, },
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,15 +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')})
} }
})
}, },
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) {
@ -128,24 +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')})
})
}, },
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')})
})
}, },
}, },

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,17 +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)
}) }
.finally(() => { finally {
this.saving = false 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,20 +93,19 @@ export default {
}, },
}, },
methods: { methods: {
markTaskAsDone(task) { async toggleTaskDone(task) {
this.loadingInternal = true this.loadingInternal = true
this.$store.dispatch('tasks/update', { try {
await this.$store.dispatch('tasks/update', {
...task, ...task,
done: !task.done, done: !task.done,
}) })
.then(() => {
if (task.done) { if (task.done) {
playPop() playPop()
} }
}) } finally {
.finally(() => {
this.loadingInternal = false this.loadingInternal = false
}) }
}, },
}, },
} }

View file

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

@ -188,14 +188,14 @@ export default {
async findTasks(query) { async findTasks(query) {
this.foundTasks = await this.taskService.getAll({}, {s: query}) this.foundTasks = await this.taskService.getAll({}, {s: query})
}, },
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] = []
} }
@ -206,40 +206,41 @@ export default {
setTimeout(() => { setTimeout(() => {
this.saved = false this.saved = false
}, 2000) }, 2000)
})
}, },
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 &&
relationKind === this.relationToDelete.relationKind
if (!found) return false
this.relatedTasks[relationKind].splice(t, 1) this.relatedTasks[relationKind].splice(t, 1)
} return true
}
}) })
this.saved = true this.saved = true
setTimeout(() => { setTimeout(() => {
this.saved = false this.saved = false
}, 2000) }, 2000)
}) } finally {
.finally(() => {
this.showDeleteModal = false 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()
})
}, },
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,50 +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 = t this.task = task
this.$emit('task-updated', t) this.$emit('task-updated', task)
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)
}, },
}]) }])
})
} }
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.$emit('task-updated', t)
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist') this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
})
}, },
hideDeferDueDatePopup(e) { hideDeferDueDatePopup(e) {
if (this.showDefer) { if (!this.showDefer) {
return
}
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => { closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
this.showDefer = false this.showDefer = false
}) })
}
}, },
}, },
} }

View file

@ -91,34 +91,34 @@ export default {
const { avatarProvider } = await this.avatarService.get({}) const { avatarProvider } = await this.avatarService.get({})
this.avatarProvider = avatarProvider this.avatarProvider = avatarProvider
}, },
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')
})
}, },
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.loading = false
this.avatarService.create(blob) return
.then(() => { }
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.$message.success({message: this.$t('user.settings.avatar.setSuccess')})
this.$store.commit('auth/reloadAvatar') this.$store.commit('auth/reloadAvatar')
}) } finally {
.finally(() => {
this.loading = false this.loading = false
this.isCropAvatar = false this.isCropAvatar = false
})
})
} else {
this.loading = false
} }
}, },
cropAvatar() { cropAvatar() {
const avatar = this.$refs.avatarUploadInput.files const avatar = this.$refs.avatarUploadInput.files

View file

@ -43,27 +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 = ''
})
}, },
}, },
} }

View file

@ -101,32 +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 = ''
})
}, },
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 = ''
})
}, },
}, },
} }

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)) onSuccess(generateAttachmentUrl(taskId, attachment.id))
}) })
if (response.errors !== null) {
throw Error(response.errors)
} }
if (r.errors !== null) {
throw Error(r.errors)
}
})
} }
export function generateAttachmentUrl(taskId: number, attachmentId: number) : any { export function generateAttachmentUrl(taskId: number, attachmentId: number) : any {

View file

@ -41,19 +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 {
const response = await HTTP.post('user/token', null, {
headers: { headers: {
Authorization: `Bearer ${getToken()}`, Authorization: `Bearer ${getToken()}`,
}, },
}) })
.then(r => { saveToken(response.data.token, persist)
saveToken(r.data.token, persist) return response
return r
}) } catch(e) {
.catch(e => {
throw new Error('Error renewing token: ', { cause: e }) throw new Error('Error renewing token: ', { cause: e })
}) }
} }

View file

@ -24,15 +24,11 @@ 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(() => {
this.reminderDates = this.reminderDates.map(d => {
d = new Date(d)
// Every time we see a reminder, we schedule a notification for it // Every time we see a reminder, we schedule a notification for it
this.scheduleNotification(d) this.reminderDates.forEach(d => this.scheduleNotification(d))
return d
})
}) })
// Parse the repeat after into something usable // Parse the repeat after into something usable
@ -218,6 +214,7 @@ export default class TaskModel extends AbstractModel {
} }
// Register the actual notification // Register the actual notification
try {
registration.showNotification('Vikunja Reminder', { registration.showNotification('Vikunja Reminder', {
tag: `vikunja-task-${this.id}`, // Group notifications by task id so we're only showing one notification per task tag: `vikunja-task-${this.id}`, // Group notifications by task id so we're only showing one notification per task
body: this.title, body: this.title,
@ -233,12 +230,10 @@ export default class TaskModel extends AbstractModel {
}, },
], ],
}) })
.then(() => {
console.debug('Notification scheduled for ' + date) console.debug('Notification scheduled for ' + date)
}) } catch(e) {
.catch(e => { throw new Error('Error scheduling notification', e)
console.debug('Error scheduling notification', e) }
})
} }
} }

View file

@ -285,32 +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}) try {
.then(response => { const response = await this.http.get(finalUrl, {params})
const result = this.modelGetFactory(response.data) const result = this.modelGetFactory(response.data)
result.maxRight = Number(response.headers['x-max-right']) result.maxRight = Number(response.headers['x-max-right'])
return result return result
}) } finally {
.finally(() => {
cancel() 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]))
} }
/** /**
@ -321,7 +319,7 @@ 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 === '') {
throw new Error('This model is not able to get data.') throw new Error('This model is not able to get data.')
} }
@ -332,22 +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 {
.then(response => { const response = await this.http.get(finalUrl, {params: params})
this.resultCount = Number(response.headers['x-pagination-result-count']) this.resultCount = Number(response.headers['x-pagination-result-count'])
this.totalPages = Number(response.headers['x-pagination-total-pages']) this.totalPages = Number(response.headers['x-pagination-total-pages'])
if (response.data === null) {
return []
}
if (Array.isArray(response.data)) { if (Array.isArray(response.data)) {
return response.data.map(entry => this.modelGetAllFactory(entry)) return response.data.map(entry => this.modelGetAllFactory(entry))
} }
if (response.data === null) {
return []
}
return this.modelGetAllFactory(response.data) return this.modelGetAllFactory(response.data)
}) } finally {
.finally(() => {
cancel() cancel()
}) }
} }
/** /**
@ -355,7 +353,7 @@ 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 === '') {
throw new Error('This model is not able to create data.') throw new Error('This model is not able to create data.')
} }
@ -363,17 +361,16 @@ export default class AbstractService {
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 {
.then(response => { const response = await this.http.put(finalUrl, model)
const result = this.modelCreateFactory(response.data) const result = this.modelCreateFactory(response.data)
if (typeof model.maxRight !== 'undefined') { if (typeof model.maxRight !== 'undefined') {
result.maxRight = model.maxRight result.maxRight = model.maxRight
} }
return result return result
}) } finally {
.finally(() => {
cancel() cancel()
}) }
} }
/** /**
@ -383,20 +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 {
.then(response => { const response = await this.http.post(url, model)
const result = this.modelUpdateFactory(response.data) const result = this.modelUpdateFactory(response.data)
if (typeof model.maxRight !== 'undefined') { if (typeof model.maxRight !== 'undefined') {
result.maxRight = model.maxRight result.maxRight = model.maxRight
} }
return result return result
}) } finally {
.finally(() => {
cancel() cancel()
}) }
} }
/** /**
@ -465,11 +461,12 @@ 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 {
const response = await this.http.put(
url, url,
formData, formData,
{ {
@ -482,10 +479,10 @@ export default class AbstractService {
}, },
}, },
) )
.then(response => this.modelCreateFactory(response.data)) this.modelCreateFactory(response.data)
.finally(() => { } finally {
this.uploadProgress = 0 this.uploadProgress = 0
cancel() cancel()
}) }
} }
} }

View file

@ -18,14 +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]))
})
} }
} }

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,28 +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 '' 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]))
})
} }
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 => response.data) const response = await this.http.delete(`/lists/${list.id}/background`, list)
.finally(() => { return response.data
} finally {
cancel() cancel()
}) }
} }
} }

View file

@ -15,25 +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 {
.then(response => { const response = await this.http.post(this.paths.reset, model)
return this.modelFactory(response.data) return this.modelFactory(response.data)
}) } finally {
.finally(() => {
cancel() cancel()
}) }
} }
requestResetPassword(model) { async requestResetPassword(model) {
const cancel = this.setLoading() const cancel = this.setLoading()
return this.http.post(this.paths.requestReset, model) try {
.then(response => { const response = await this.http.post(this.paths.requestReset, model)
return this.modelFactory(response.data) return this.modelFactory(response.data)
}) } finally {
.finally(() => {
cancel() cancel()
}) }
} }
} }

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,15 +94,14 @@ 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')
}) } catch(e) {
.catch(e => {
if ( if (
e.response && e.response &&
e.response.data.code === 1017 && e.response.data.code === 1017 &&
@ -112,35 +111,34 @@ export default {
} }
throw 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 {
await HTTP.post('register', {
username: credentials.username, username: credentials.username,
email: credentials.email, email: credentials.email,
password: credentials.password, password: credentials.password,
}) })
.then(() => {
return ctx.dispatch('login', credentials) return ctx.dispatch('login', credentials)
}) } catch(e) {
.catch(e => {
if (e.response && e.response.data && e.response.data.message) { if (e.response && e.response.data && e.response.data.message) {
ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true}) ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true})
} }
throw 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,29 +148,28 @@ 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')
}) } finally {
.finally(() => {
ctx.commit(LOADING, false, {root: true}) 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 r.data return response.data
})
}, },
// 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) {
@ -205,54 +202,54 @@ export default {
ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true}) ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true})
} }
}, },
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', { const response = await HTTP.get('user', {
headers: { headers: {
Authorization: `Bearer ${jwt}`, Authorization: `Bearer ${jwt}`,
}, },
}) })
.then(r => { const info = new UserModel(response.data)
const info = new UserModel(r.data)
info.type = ctx.state.info.type info.type = ctx.state.info.type
info.email = ctx.state.info.email info.email = ctx.state.info.email
info.exp = ctx.state.info.exp info.exp = ctx.state.info.exp
ctx.commit('info', info) ctx.commit('info', info)
ctx.commit('lastUserRefresh') ctx.commit('lastUserRefresh')
}) return info
.catch(e => { } catch(e) {
throw new Error('Error while refreshing user info:', { cause: 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,15 +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 r
})
}, },
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,13 +218,14 @@ 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 r return response
}) } finally {
.finally(() => cancel()) cancel()
}
}, },
async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) { async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) {
@ -270,48 +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('addTasksToBucket', {tasks, bucketId: bucketId})
ctx.commit('setTasksLoadedForBucketPage', {bucketId, page}) ctx.commit('setTasksLoadedForBucketPage', {bucketId, page})
if (taskService.totalPages <= page) { if (taskService.totalPages <= page) {
ctx.commit('setAllTasksLoadedForBucket', bucketId) ctx.commit('setAllTasksLoadedForBucket', bucketId)
} }
return r return tasks
}) } finally {
.finally(() => {
cancel() cancel()
ctx.commit('setBucketLoading', {bucketId: bucketId, loading: false}) 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 r return createdBucket
}) } finally {
.finally(() => cancel()) 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 r return response
}) } finally {
.finally(() => cancel()) 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)
@ -325,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})
throw 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) {
@ -352,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

@ -42,25 +42,28 @@ 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 r return createdList
}) } finally {
.finally(() => cancel()) 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})
@ -78,29 +81,31 @@ export default {
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 newList return newList
}) } catch(e) {
.catch(e => {
// Reset the list state to the initial one to avoid confusion for the user // Reset the list state to the initial one to avoid confusion for the user
ctx.commit('setList', { ctx.commit('setList', {
...list, ...list,
isFavorite: !list.isFavorite, isFavorite: !list.isFavorite,
}) })
throw e throw e
}) } finally {
.finally(() => cancel()) 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 r return response
}) } finally{
.finally(() => cancel()) cancel()
}
}, },
}, },
} }

View file

@ -94,63 +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()
try {
// We always load all namespaces and filter them on the frontend // We always load all namespaces and filter them on the frontend
return namespaceService.getAll({}, {is_archived: true}) const namespaces = await namespaceService.getAll({}, {is_archived: true})
.then(r => { ctx.commit('namespaces', namespaces)
ctx.commit('namespaces', r)
// 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 r return namespaces
}) } finally {
.finally(() => {
cancel() 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)
} }
}, },
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 r return response
}) } finally {
.finally(() => cancel()) 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 r return createdNamespace
}) } finally {
.finally(() => cancel()) cancel()
}
}, },
}, },
} }

View file

@ -34,17 +34,15 @@ 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 result return response
})
} }
async function findAssignees(parsedTaskAssignees) { async function findAssignees(parsedTaskAssignees) {
@ -67,41 +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 {
.finally(() => {
cancel() 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 t return updatedTask
}) } finally {
.finally(() => {
cancel() 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 t return response
})
}, },
// 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}) {
@ -124,12 +120,11 @@ 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
@ -142,22 +137,20 @@ export default {
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 r
})
}, },
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 r return response
} }
for (const a in t.task.assignees) { for (const a in t.task.assignees) {
@ -169,17 +162,15 @@ export default {
} }
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return r return response
})
}, },
addLabel(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.create(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
@ -191,24 +182,21 @@ export default {
// FIXME: direct store manipulation (task) // FIXME: direct store manipulation (task)
t.task.labels.push(label) t.task.labels.push(label)
ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true }) ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true })
return r return r
})
}, },
removeLabel(ctx, {label, taskId}) { async removeLabel(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 response = await labelTaskService.delete(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 remove label from task in kanban, task not found', t) console.debug('Could not remove label from task in kanban, task not found', t)
return r return response
} }
// Remove the label from the list // Remove the label from the list
@ -222,8 +210,7 @@ export default {
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
return r 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
@ -308,11 +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,
})) })
}, },
}, },
} }

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,13 +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()}})
})
}, },
}, },
} }

View file

@ -24,17 +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'})
})
}, },
}, },
} }

View file

@ -95,27 +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.filter = r
this.filters = objectToSnakeCase(this.filter.filters) this.filters = objectToSnakeCase(this.filter.filters)
})
}, },
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 = r this.filter = filter
this.filters = objectToSnakeCase(this.filter.filters) this.filters = objectToSnakeCase(this.filter.filters)
this.$router.back() this.$router.back()
})
}, },
}, },
} }

View file

@ -60,21 +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: r.id}, params: {id: label.id},
}) })
this.$message.success({message: this.$t('label.create.success')}) this.$message.success({message: this.$t('label.create.success')})
})
}, },
}, },
} }

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,14 +62,11 @@ 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)
.then((r) => {
this.$message.success({message: this.$t('list.create.createdSuccess') }) this.$message.success({message: this.$t('list.create.createdSuccess') })
this.$router.push({ this.$router.push({
name: 'list.index', name: 'list.index',
params: { listId: r.id }, params: { listId: list.id },
})
}) })
}, },
}, },

View file

@ -83,7 +83,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,14 +140,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) this.$store.commit(CURRENT_LIST, loadedList)
this.setTitle(this.getListTitle(r)) this.setTitle(this.getListTitle(loadedList))
}) } finally {
.finally(() => {
this.listLoaded = this.$route.params.listId this.listLoaded = this.$route.params.listId
}) }
}, },
}, },
} }

View file

@ -30,21 +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 {
.finally(() => {
this.$router.back() this.$router.back()
}) }
}, },
}, },
} }

View file

@ -71,6 +71,10 @@ 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'
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export default { export default {
name: 'list-setting-background', name: 'list-setting-background',
components: {CreateEdit}, components: {CreateEdit},
@ -108,61 +112,53 @@ export default {
this.backgroundThumbs = {} this.backgroundThumbs = {}
this.searchBackgrounds() this.searchBackgrounds()
}, },
searchBackgrounds(page = 1) {
async searchBackgrounds(page = 1) {
if (this.backgroundSearchTimeout !== null) { if (this.backgroundSearchTimeout !== null) {
clearTimeout(this.backgroundSearchTimeout) clearTimeout(this.backgroundSearchTimeout)
} }
// We're using the timeout to not search on every keypress but with a 300ms delay. // TODO: use throttle
// FIXME: We're using the timeout to not search on every keypress but with a 300ms delay.
// If another key is pressed within these 300ms, the last search request is dropped and a new one is scheduled. // If another key is pressed within these 300ms, the last search request is dropped and a new one is scheduled.
this.backgroundSearchTimeout = setTimeout(() => { this.backgroundSearchTimeout = await timeout(300)
this.currentPage = page this.currentPage = page
this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page}) const r = await this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
.then(r => {
this.backgroundSearchResult = this.backgroundSearchResult.concat(r) this.backgroundSearchResult = this.backgroundSearchResult.concat(r)
r.forEach(b => { r.forEach(async b => {
this.backgroundService.thumb(b) this.backgroundThumbs[b.id] = await this.backgroundService.thumb(b)
.then(t => {
this.backgroundThumbs[b.id] = t
}) })
})
})
}, 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 => { this.$store.commit(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')})
})
}, },
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 => { this.$store.commit(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')})
})
}, },
removeBackground() {
this.listService.removeBackground(this.currentList) async removeBackground() {
.then(l => { const list = await this.listService.removeBackground(this.currentList)
this.$store.commit(CURRENT_LIST, l) this.$store.commit(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()
})
}, },
}, },
} }

View file

@ -24,12 +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'})
})
}, },
}, },
} }

View file

@ -38,18 +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: r.list.id}}) this.$router.push({name: 'list.index', params: {listId: duplicate.list.id}})
})
}, },
}, },
} }

View file

@ -94,22 +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 }
})
}, },
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) this.$store.commit(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()
})
}, },
}, },
} }

View file

@ -56,18 +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 => { this.$store.commit(CURRENT_LIST, this.list)
this.list = r
this.$store.commit(CURRENT_LIST, r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'userTeam' this.manageTeamsComponent = 'userTeam'
this.manageUsersComponent = 'userTeam' this.manageUsersComponent = 'userTeam'
this.setTitle(this.$t('list.share.title', {list: this.list.title})) this.setTitle(this.$t('list.share.title', {list: this.list.title}))
})
}, },
}, },
} }

View file

@ -408,7 +408,7 @@ export default {
this.$store.dispatch('kanban/updateBucket', newBucket) this.$store.dispatch('kanban/updateBucket', 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
@ -427,34 +427,35 @@ export default {
kanbanPosition: calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null), 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)
} finally {
this.taskUpdating[task.id] = false this.taskUpdating[task.id] = false
this.oneTaskUpdating = 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', r) this.$store.commit('kanban/addTaskToBucket', task)
this.scrollTaskContainerToBottom(bucketId) this.scrollTaskContainerToBottom(bucketId)
})
}, },
scrollTaskContainerToBottom(bucketId) { scrollTaskContainerToBottom(bucketId) {
const bucketEl = this.taskContainerRefs[bucketId] const bucketEl = this.taskContainerRefs[bucketId]
if (!bucketEl) { if (!bucketEl) {
@ -462,7 +463,8 @@ export default {
} }
bucketEl.scrollTop = bucketEl.scrollHeight bucketEl.scrollTop = bucketEl.scrollHeight
}, },
createNewBucket() {
async createNewBucket() {
if (this.newBucketTitle === '') { if (this.newBucketTitle === '') {
return return
} }
@ -472,12 +474,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
@ -486,33 +487,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(() => {
this.showBucketDeleteModal = false
}) })
try {
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) {
@ -535,7 +542,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
} }
@ -545,27 +553,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')})
}, },
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

@ -291,7 +291,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]
@ -303,10 +304,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
})
}, },
}, },
} }

View file

@ -63,20 +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 = this.namespaceService.create(this.namespace)
.create(this.namespace) this.$store.commit('namespaces/addNamespace', namespace)
.then((r) => {
this.$store.commit('namespaces/addNamespace', r)
this.$message.success({message: this.$t('namespace.create.success') }) this.$message.success({message: this.$t('namespace.create.success') })
this.$router.back() this.$router.back()
})
}, },
}, },
} }

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,19 +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 {
.finally(() => {
this.$router.back() this.$router.back()
}) }
}, },
}, },
} }

View file

@ -35,12 +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'})
})
}, },
}, },
} }

View file

@ -93,8 +93,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,24 +103,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.namespace = r
// This will trigger the dynamic loading of components once we actually have all the data to pass to them // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing' this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing' this.manageUsersComponent = 'manageSharing'
this.title = this.$t('namespace.edit.title', {namespace: r.title}) this.title = this.$t('namespace.edit.title', {namespace: this.namespace.title})
this.setTitle(this.title) this.setTitle(this.title)
})
}, },
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()
})
}, },
}, },
} }

View file

@ -57,17 +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.namespace = r
// This will trigger the dynamic loading of components once we actually have all the data to pass to them // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing' this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing' this.manageUsersComponent = 'manageSharing'
this.title = this.$t('namespace.share.title', { namespace: this.namespace.title }) this.title = this.$t('namespace.share.title', { namespace: this.namespace.title })
this.setTitle(this.title) this.setTitle(this.title)
})
}, },
}, },
} }

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,11 +66,13 @@ 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}})
} catch(e) {
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) { if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
this.authenticateWithPassword = true this.authenticateWithPassword = true
return return
@ -85,10 +87,9 @@ export default {
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

@ -141,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.
@ -163,7 +163,10 @@ export default {
if (this.showAll) { if (this.showAll) {
this.setTitle(this.$t('task.show.titleCurrent')) this.setTitle(this.$t('task.show.titleCurrent'))
} else { } else {
this.setTitle(this.$t('task.show.titleDates', { from: this.cStartDate.toLocaleDateString(), to: this.cEndDate.toLocaleDateString()})) this.setTitle(this.$t('task.show.titleDates', {
from: this.cStartDate.toLocaleDateString(),
to: this.cEndDate.toLocaleDateString(),
}))
} }
const params = { const params = {
@ -197,21 +200,19 @@ export default {
} }
} }
this.$store.dispatch('tasks/loadTasks', params) const tasks = await this.$store.dispatch('tasks/loadTasks', params)
.then(r => {
// FIXME: sort tasks in computed
// Sort all tasks to put those with a due date before the ones without a due date, the // Sort all tasks to put those with a due date before the ones without a due date, the
// soonest before the later ones. // soonest before the later ones.
// We can't use the api sorting here because that sorts tasks with a due date after // We can't use the api sorting here because that sorts tasks with a due date after
// ones without a due date. // ones without a due date.
r.sort((a, b) => { this.tasks = tasks.sort((a, b) => {
return a.dueDate === null && b.dueDate === null ? -1 : 1 return Number(b.dueDate === null) - Number(a.dueDate === null)
})
const tasks = r.filter(t => t.dueDate !== null).concat(r.filter(t => t.dueDate === null))
this.tasks = tasks
}) })
}, },
// 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) {
@ -225,18 +226,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

@ -561,23 +561,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 {
.finally(() => {
this.$nextTick(() => this.visible = true)
this.scrollToHeading() this.scrollToHeading()
}) await this.$nextTick()
this.visible = true
}
}, },
scrollToHeading() { scrollToHeading() {
this.$refs.heading.$el.scrollIntoView({block: 'center'}) this.$refs.heading.$el.scrollIntoView({block: 'center'})
@ -633,6 +632,7 @@ export default {
} }
this.$message.success({message: this.$t('task.detail.updateSuccess')}, actions) 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(() => {
@ -651,13 +651,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}})
})
}, },
toggleTaskDone() { toggleTaskDone() {
this.task.done = !this.task.done this.task.done = !this.task.done
@ -665,36 +665,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.task = t
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist') this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
})
}, },
}, },
} }

View file

@ -219,68 +219,57 @@ 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)
.then((response) => {
this.team = response
this.title = this.$t('team.edit.title', {team: this.team.name}) this.title = this.$t('team.edit.title', {team: this.team.name})
this.setTitle(this.title) this.setTitle(this.title)
})
}, },
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)
.then((response) => {
this.team = response
this.$message.success({message: this.$t('team.edit.success')}) this.$message.success({message: this.$t('team.edit.success')})
})
}, },
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'})
})
}, },
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 {
.finally(() => {
this.showUserDeleteModal = false 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)
.then(() => {
this.loadTeam() this.loadTeam()
this.$message.success({message: this.$t('team.edit.userAddedSuccess')}) this.$message.success({message: this.$t('team.edit.userAddedSuccess')})
})
}, },
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)
.then((r) => {
for (const tm in this.team.members) { for (const tm in this.team.members) {
if (this.team.members[tm].id === member.id) { if (this.team.members[tm].id === member.id) {
this.team.members[tm].admin = r.admin this.team.members[tm].admin = r.admin
@ -292,20 +281,17 @@ export default {
this.$t('team.edit.madeAdmin') : this.$t('team.edit.madeAdmin') :
this.$t('team.edit.madeMember'), this.$t('team.edit.madeMember'),
}) })
})
}, },
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
})
}, },
clearAll() { clearAll() {
this.foundUsers = [] this.foundUsers = []
}, },

View file

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

View file

@ -49,22 +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)
.then((response) => {
this.$router.push({ this.$router.push({
name: 'teams.edit', name: 'teams.edit',
params: { id: response.id }, params: { id: response.id },
}) })
this.$message.success({message: this.$t('team.create.success') }) this.$message.success({message: this.$t('team.create.success') })
})
}, },
}, },
} }

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,11 +202,10 @@ 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
} }
@ -217,8 +217,9 @@ export default {
} }
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,11 +59,11 @@ export default {
this.$store.commit(ERROR_MESSAGE, '') this.$store.commit(ERROR_MESSAGE, '')
this.$store.dispatch('auth/openIdAuth', { try {
await this.$store.dispatch('auth/openIdAuth', {
provider: this.$route.params.provider, provider: this.$route.params.provider,
code: this.$route.query.code, code: this.$route.query.code,
}) })
.then(() => {
const last = getLastVisited() const last = getLastVisited()
if (last !== null) { if (last !== null) {
this.$router.push({ this.$router.push({
@ -74,8 +74,7 @@ export default {
} else { } else {
this.$router.push({name: 'home'}) this.$router.push({name: 'home'})
} }
}) } catch(e) {
.catch(e => {
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])
@ -83,10 +82,9 @@ export default {
} }
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

@ -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,33 +385,29 @@ 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')})
})
}, },
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')})
})
}, },
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) {
.catch(e => {
// Error code 1016 means totp is not enabled, we don't need an error in that case. // 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) { if (e.response && e.response.data && e.response.data.code && e.response.data.code === 1016) {
this.totpEnrolled = false this.totpEnrolled = false
@ -419,50 +415,45 @@ export default {
} }
throw e throw e
}) }
}, },
totpSetQrCode() {
this.totpService.qrcode() async totpSetQrCode() {
.then(qr => { const qr = await this.totpService.qrcode()
const urlCreator = window.URL || window.webkitURL const urlCreator = window.URL || window.webkitURL
this.totpQR = urlCreator.createObjectURL(qr) this.totpQR = urlCreator.createObjectURL(qr)
})
}, },
totpEnroll() {
this.totpService.enroll() async totpEnroll() {
.then(r => { this.totp = await this.totpService.enroll()
this.totpEnrolled = true this.totpEnrolled = true
this.totp = r
this.totpSetQrCode() this.totpSetQrCode()
})
}, },
totpConfirm() {
this.totpService.enable({passcode: this.totpConfirmPasscode}) async totpConfirm() {
.then(() => { await this.totpService.enable({passcode: this.totpConfirmPasscode})
this.totp.enabled = true this.totp.enabled = true
this.$message.success({message: this.$t('user.settings.totp.confirmSuccess')}) this.$message.success({message: this.$t('user.settings.totp.confirmSuccess')})
})
}, },
totpDisable() {
this.totpService.disable({password: this.totpDisablePassword}) async totpDisable() {
.then(() => { await this.totpService.disable({password: this.totpDisablePassword})
this.totpEnrolled = false this.totpEnrolled = false
this.totp = new TotpModel() this.totp = new TotpModel()
this.$message.success({message: this.$t('user.settings.totp.disableSuccess')}) this.$message.success({message: this.$t('user.settings.totp.disableSuccess')})
})
}, },
updateSettings() {
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')})
})
}, },
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))