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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -215,59 +215,41 @@ export default {
frontendUrl: (state) => state.config.frontendUrl,
}),
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) {
return
}
this.linkShareService
.getAll({listId})
.then((r) => {
this.linkShares = r
})
.catch((e) => {
this.$message.error(e)
})
this.linkShares = await this.linkShareService.getAll({listId})
},
add(listId) {
async add(listId) {
const newLinkShare = new LinkShareModel({
right: this.selectedRight,
listId,
name: this.name,
password: this.password,
})
this.linkShareService
.create(newLinkShare)
.then(() => {
await this.linkShareService.create(newLinkShare)
this.selectedRight = rights.READ
this.name = ''
this.password = ''
this.showNewForm = false
this.$message.success({message: this.$t('list.share.links.createSuccess')})
this.load(listId)
})
.catch((e) => {
this.$message.error(e)
})
await this.load(listId)
},
remove(listId) {
async remove(listId) {
const linkshare = new LinkShareModel({
id: this.linkIdToDelete,
listId,
})
this.linkShareService
.delete(linkshare)
.then(() => {
try {
await this.linkShareService.delete(linkshare)
this.$message.success({message: this.$t('list.share.links.deleteSuccess')})
this.load(listId)
})
.catch((e) => {
this.$message.error(e)
})
.finally(() => {
await this.load(listId)
} finally {
this.showDeleteModal = false
})
}
},
copy,
getShareLink(hash) {

View file

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

View file

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

View file

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

View file

@ -297,33 +297,26 @@ export default {
console.debug('prepareGanttDays; years:', years)
this.days = years
},
parseTasks() {
this.setDates()
this.loadTasks()
},
loadTasks() {
async loadTasks() {
this.theTasks = []
this.tasksWithoutDates = []
const getAllTasks = (page = 1) => {
return this.taskCollectionService
.getAll({listId: this.listId}, this.params, page)
.then((tasks) => {
const getAllTasks = async (page = 1) => {
const tasks = await this.taskCollectionService.getAll({listId: this.listId}, this.params, page)
if (page < this.taskCollectionService.totalPages) {
return getAllTasks(page + 1).then((nextTasks) => {
const nextTasks = await getAllTasks(page + 1)
return tasks.concat(nextTasks)
})
} else {
}
return tasks
}
})
.catch((e) => {
return Promise.reject(e)
})
}
getAllTasks()
.then((tasks) => {
const tasks = await getAllTasks()
this.theTasks = tasks
.filter((t) => {
if (t.startDate === null && !t.done) {
@ -334,18 +327,12 @@ export default {
t.endDate <= this.endDate
)
})
.map((t) => {
return this.addGantAttributes(t)
})
.map((t) => this.addGantAttributes(t))
.sort(function (a, b) {
if (a.startDate < b.startDate) return -1
if (a.startDate > b.startDate) return 1
return 0
})
})
.catch((e) => {
this.$message.error(e)
})
},
addGantAttributes(t) {
if (typeof t.durationDays !== 'undefined' && typeof t.offsetDays !== 'undefined') {
@ -357,7 +344,7 @@ export default {
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24)
return t
},
resizeTask(taskDragged, newRect) {
async resizeTask(taskDragged, newRect) {
if (this.isTaskEdit) {
return
}
@ -398,9 +385,7 @@ export default {
offsetDays: newTask.offsetDays,
}
this.taskService
.update(newTask)
.then(r => {
const r = await this.taskService.update(newTask)
r.endDate = ganttData.endDate
r.durationDays = ganttData.durationDays
r.offsetDays = ganttData.offsetDays
@ -422,10 +407,6 @@ export default {
}
}
}
})
.catch((e) => {
this.$message.error(e)
})
},
editTask(task) {
this.taskToEdit = task
@ -445,7 +426,7 @@ export default {
this.$nextTick(() => (this.newTaskFieldActive = false))
}
},
addNewTask() {
async addNewTask() {
if (!this.newTaskFieldActive) {
return
}
@ -453,16 +434,10 @@ export default {
title: this.newTaskTitle,
listId: this.listId,
})
this.taskService
.create(task)
.then((r) => {
const r = await this.taskService.create(task)
this.tasksWithoutDates.push(this.addGantAttributes(r))
this.newTaskTitle = ''
this.hideCrateNewTask()
})
.catch((e) => {
this.$message.error(e)
})
},
formatYear(date) {
return this.format(date, 'MMMM, yyyy')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,24 +11,22 @@ export function uploadFile(taskId: number, file: FileModel, onSuccess: () => Fun
return uploadFiles(attachmentService, taskId, files, onSuccess)
}
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})
attachmentService.create(attachmentModel, files)
.then(r => {
console.debug(`Uploaded attachments for task ${taskId}, response was`, r)
if (r.success !== null) {
r.success.forEach((attachment: AttachmentModel) => {
const response = await attachmentService.create(attachmentModel, files)
console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
response.success?.map((attachment: AttachmentModel) => {
store.dispatch('tasks/addTaskAttachment', {
taskId,
attachment,
})
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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,26 +42,28 @@ export default {
isFavorite: !list.isFavorite,
})
},
createList(ctx, list) {
async createList(ctx, list) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
return listService.create(list)
.then(r => {
r.namespaceId = list.namespaceId
ctx.commit('namespaces/addListToNamespace', r, {root: true})
ctx.commit('setList', r)
return Promise.resolve(r)
})
.catch(e => Promise.reject(e))
.finally(() => cancel())
try {
const createdList = await listService.create(list)
createdList.namespaceId = list.namespaceId
ctx.commit('namespaces/addListToNamespace', createdList, {root: true})
ctx.commit('setList', createdList)
return createdList
} finally {
cancel()
}
},
updateList(ctx, list) {
async updateList(ctx, list) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
return listService.update(list)
.then(() => {
try {
await listService.update(list)
ctx.commit('setList', list)
ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
@ -78,33 +80,32 @@ export default {
}
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
return Promise.resolve(newList)
})
.catch(e => {
return newList
} catch(e) {
// Reset the list state to the initial one to avoid confusion for the user
ctx.commit('setList', {
...list,
isFavorite: !list.isFavorite,
})
return Promise.reject(e)
})
.finally(() => cancel())
throw e
} finally {
cancel()
}
},
deleteList(ctx, list) {
async deleteList(ctx, list) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
return listService.delete(list)
.then(r => {
try {
const response = await listService.delete(list)
ctx.commit('removeListById', list)
ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true})
removeListFromHistory({id: list.id})
return Promise.resolve(r)
})
.catch(e => {
return Promise.reject(e)
})
.finally(() => cancel())
return response
} finally{
cancel()
}
},
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -51,9 +51,6 @@ export default {
listLoaded: 0,
}
},
mounted() {
this.loadList()
},
watch: {
// call again the method if the route changes
'$route.path': {
@ -83,7 +80,8 @@ export default {
this.$router.replace({name: savedListView, params: {id: this.$route.params.listId}})
console.debug('Replaced list view with', savedListView)
},
loadList() {
async loadList() {
if (this.$route.name.includes('.settings.')) {
return
}
@ -139,17 +137,13 @@ export default {
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
this.listService.get(list)
.then(r => {
this.$store.dispatch(CURRENT_LIST, r)
this.setTitle(this.getListTitle(r))
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
try {
const loadedList = await this.listService.get(list)
await this.$store.dispatch(CURRENT_LIST, loadedList)
this.setTitle(this.getListTitle(loadedList))
} finally {
this.listLoaded = this.$route.params.listId
})
}
},
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,6 @@
<div class="control">
<editor
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
:preview-is-default="false"
id="namespacedescription"
:placeholder="$t('namespace.attributes.descriptionPlaceholder')"
@ -93,8 +92,8 @@ export default {
},
},
methods: {
loadNamespace() {
// This makes the editor trigger its mounted function again which makes it forget every input
async loadNamespace() {
// 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
// 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.
@ -103,30 +102,20 @@ export default {
this.$nextTick(() => this.editorActive = true)
const namespace = new NamespaceModel({id: this.$route.params.id})
this.namespaceService.get(namespace)
.then(r => {
this.namespace = r
this.namespace = await this.namespaceService.get(namespace)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing'
this.title = this.$t('namespace.edit.title', {namespace: r.title})
this.title = this.$t('namespace.edit.title', {namespace: this.namespace.title})
this.setTitle(this.title)
})
.catch(e => {
this.$message.error(e)
})
},
save() {
this.namespaceService.update(this.namespace)
.then(r => {
async save() {
const namespace = await this.namespaceService.update(this.namespace)
// 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.$router.back()
})
.catch(e => {
this.$message.error(e)
})
},
},
}

View file

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

View file

@ -57,7 +57,7 @@ export default {
'authLinkShare',
]),
methods: {
auth() {
async auth() {
this.errorMessage = ''
if (this.authLinkShare) {
@ -66,11 +66,13 @@ export default {
this.loading = true
this.$store.dispatch('auth/linkShareAuth', {hash: this.$route.params.share, password: this.password})
.then((r) => {
this.$router.push({name: 'list.list', params: {listId: r.list_id}})
try {
const r = await this.$store.dispatch('auth/linkShareAuth', {
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) {
this.authenticateWithPassword = true
return
@ -85,10 +87,9 @@ export default {
errorMessage = this.$t('sharing.invalidPassword')
}
this.errorMessage = errorMessage
})
.finally(() => {
} finally {
this.loading = false
})
}
},
},
}

View file

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

View file

@ -564,26 +564,22 @@ export default {
return uploadFile(this.taskId, ...args)
},
loadTask(taskId) {
async loadTask(taskId) {
if (taskId === undefined) {
return
}
this.taskService.get({id: taskId})
.then(r => {
this.task = r
this.$store.commit('attachments/set', r.attachments)
try {
this.task = await this.taskService.get({id: taskId})
this.$store.commit('attachments/set', this.task.attachments)
this.taskColor = this.task.hexColor
this.setActiveFields()
this.setTitle(this.task.title)
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.$nextTick(() => this.visible = true)
} finally {
this.scrollToHeading()
})
await this.$nextTick()
this.visible = true
}
},
scrollToHeading() {
this.$refs.heading.$el.scrollIntoView({block: 'center'})
@ -623,7 +619,6 @@ export default {
this.task.endDate = this.task.dueDate
}
try {
this.task = await this.$store.dispatch('tasks/update', this.task)
this.setActiveFields()
@ -639,10 +634,8 @@ export default {
}]
}
this.$message.success({message: this.$t('task.detail.updateSuccess')}, actions)
} catch(e) {
this.$message.error(e)
}
},
setFieldActive(fieldName) {
this.activeFields[fieldName] = true
this.$nextTick(() => {
@ -661,16 +654,13 @@ export default {
}
})
},
deleteTask() {
this.$store.dispatch('tasks/delete', this.task)
.then(() => {
async deleteTask() {
await this.$store.dispatch('tasks/delete', this.task)
this.$message.success({message: this.$t('task.detail.deleteSuccess')})
this.$router.push({name: 'list.index', params: {listId: this.task.listId}})
})
.catch(e => {
this.$message.error(e)
})
},
toggleTaskDone() {
this.task.done = !this.task.done
@ -678,39 +668,26 @@ export default {
playPop()
}
this.saveTask(true, () => this.toggleTaskDone())
this.saveTask(true, this.toggleTaskDone)
},
setDescriptionChanged(e) {
if (e.key === 'Enter' || e.key === 'Control') {
return
}
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) {
this.$store.commit('kanban/removeTaskInBucket', this.task)
this.task.listId = list.id
await this.saveTask()
},
toggleFavorite() {
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite
this.taskService.update(this.task)
.then(t => {
this.task = t
this.task = await this.taskService.update(this.task)
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
})
.catch(e => {
this.$message.error(e)
})
},
},
}

View file

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

View file

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

View file

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

View file

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

View file

@ -187,7 +187,8 @@ export default {
this.loading = false
}
},
submit() {
async submit() {
this.$store.commit(ERROR_MESSAGE, '')
// 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.
@ -201,11 +202,10 @@ export default {
credentials.totpPasscode = this.$refs.totpPasscode.value
}
this.$store.dispatch('auth/login', credentials)
.then(() => {
try {
await this.$store.dispatch('auth/login', credentials)
this.$store.commit('auth/needsTotpPasscode', false)
})
.catch(e => {
} catch(e) {
if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) {
return
}
@ -217,8 +217,9 @@ export default {
}
this.$store.commit(ERROR_MESSAGE, err[0])
})
}
},
redirectToProvider(provider) {
redirectToProvider(provider, this.openidConnect.redirectUrl)
},

View file

@ -26,7 +26,7 @@ export default {
this.authenticateWithCode()
},
methods: {
authenticateWithCode() {
async authenticateWithCode() {
// 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
// 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.dispatch('auth/openIdAuth', {
try {
await this.$store.dispatch('auth/openIdAuth', {
provider: this.$route.params.provider,
code: this.$route.query.code,
})
.then(() => {
const last = getLastVisited()
if (last !== null) {
this.$router.push({
@ -74,8 +74,7 @@ export default {
} else {
this.$router.push({name: 'home'})
}
})
.catch(e => {
} catch(e) {
const err = getErrorText(e)
if (typeof err[1] !== 'undefined') {
this.$store.commit(ERROR_MESSAGE, err[1])
@ -83,10 +82,9 @@ export default {
}
this.$store.commit(ERROR_MESSAGE, err[0])
})
.finally(() => {
} finally {
localStorage.removeItem('authenticating')
})
}
},
},
}

View file

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

View file

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

View file

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

View file

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