diff --git a/package.json b/package.json index a51d874e..dc680846 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.vue b/src/App.vue index 90231eb0..d2690c26 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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 - localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset) - this.$router.push({name: 'user.password-reset.reset'}) + 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 - localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm) - this.$router.push({name: 'user.login'}) + 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') { - const accountDeletionService = new AccountDeleteService() - accountDeletionService.confirm(this.$route.query.accountDeletionConfirm) - .then(() => { - this.$message.success({message: this.$t('user.deletion.confirmSuccess')}) - this.$store.dispatch('auth/refreshUserInfo') - }) - .catch(e => this.$message.error(e)) + async setupAccountDeletionVerification() { + if (typeof this.$route.query.accountDeletionConfirm === 'undefined') { + return } + + const accountDeletionService = new AccountDeleteService() + await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm) + this.$message.success({message: this.$t('user.deletion.confirmSuccess')}) + this.$store.dispatch('auth/refreshUserInfo') }, }, }) diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue index ff055830..b4b6c527 100644 --- a/src/components/home/contentAuth.vue +++ b/src/components/home/contentAuth.vue @@ -128,9 +128,6 @@ export default { }, loadLabels() { this.$store.dispatch('labels/loadAllLabels') - .catch(e => { - this.$message.error(e) - }) }, }, } diff --git a/src/components/home/navigation.vue b/src/components/home/navigation.vue index 44a3c9a0..16258985 100644 --- a/src/components/home/navigation.vue +++ b/src/components/home/navigation.vue @@ -54,14 +54,14 @@ + v-tooltip="namespaceTitles[nk]"> - {{ getNamespaceTitle(n) }} ({{ n.lists.filter(l => !l.isArchived).length }}) + {{ namespaceTitles[nk] }} @@ -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) - // create a copy of the list in order to not violate vuex mutations - this.$store.dispatch('lists/updateList', { - ...list, - position, - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.listUpdating[list.id] = false + try { + // create a copy of the list in order to not violate vuex mutations + await this.$store.dispatch('lists/updateList', { + ...list, + position, }) + } finally { + this.listUpdating[list.id] = false + } }, }, } diff --git a/src/components/input/editor.vue b/src/components/input/editor.vue index d51ed19f..a2042db0 100644 --- a/src/components/input/editor.vue +++ b/src/components/input/editor.vue @@ -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 => { - img.src = url - this.loadedAttachments[cacheKey] = url - }) + const url = await this.attachmentService.getBlobUrl(attachment) + img.src = url + this.loadedAttachments[cacheKey] = url } } diff --git a/src/components/list/partials/filters.vue b/src/components/list/partials/filters.vue index e27cee12..7360e616 100644 --- a/src/components/list/partials/filters.vue +++ b/src/components/list/partials/filters.vue @@ -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 => { - // 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) - }) + 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)) }, add(kind, filterName) { this.$nextTick(() => { diff --git a/src/components/list/partials/list-card.vue b/src/components/list/partials/list-card.vue index 09be012a..f54f1117 100644 --- a/src/components/list/partials/list-card.vue +++ b/src/components/list/partials/list-card.vue @@ -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)) }, }, } diff --git a/src/components/migrator/migration.vue b/src/components/migrator/migration.vue index 9846f7ef..5208d421 100644 --- a/src/components/migrator/migration.vue +++ b/src/components/migrator/migration.vue @@ -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 = '' + let migrationConfig = { code: this.migratorAuthCode } + if (this.isFileMigrator) { - return this.migrateFile() + if (this.$refs.uploadInput.files.length === 0) { + return + } + migrationConfig = this.$refs.uploadInput.files[0] } - this.migrationService.migrate({code: this.migratorAuthCode}) - .then(r => { - this.message = r.message - this.$store.dispatch('namespaces/loadNamespaces') - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.isMigrating = false - }) - }, - migrateFile() { - if (this.$refs.uploadInput.files.length === 0) { - return + try { + const { message } = await this.migrationService.migrate(migrationConfig) + this.message = message + return this.$store.dispatch('namespaces/loadNamespaces') + } finally { + this.isMigrating = false } - - this.migrationService.migrate(this.$refs.uploadInput.files[0]) - .then(r => { - this.message = r.message - this.$store.dispatch('namespaces/loadNamespaces') - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.isMigrating = false - }) }, }, } diff --git a/src/components/misc/api-config.vue b/src/components/misc/api-config.vue index 06312ca3..acf36df8 100644 --- a/src/components/misc/api-config.vue +++ b/src/components/misc/api-config.vue @@ -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 diff --git a/src/components/misc/subscription.vue b/src/components/misc/subscription.vue index f29091c6..8865c43f 100644 --- a/src/components/misc/subscription.vue +++ b/src/components/misc/subscription.vue @@ -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(() => { - this.$emit('change', subscription) - this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})}) - }) - .catch(e => { - this.$message.error(e) - }) + await this.subscriptionService.create(subscription) + this.$emit('change', subscription) + this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})}) }, - unsubscribe() { + async unsubscribe() { const subscription = new SubscriptionModel({ entity: this.entity, entityId: this.entityId, }) - this.subscriptionService.delete(subscription) - .then(() => { - this.$emit('change', null) - this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})}) - }) - .catch(e => { - this.$message.error(e) - }) + await this.subscriptionService.delete(subscription) + this.$emit('change', null) + this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})}) }, }, } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 0fb3d4c6..bd185511 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -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) } }, }, diff --git a/src/components/quick-actions/quick-actions.vue b/src/components/quick-actions/quick-actions.vue index 5ed2fa1b..51fc8daa 100644 --- a/src/components/quick-actions/quick-actions.vue +++ b/src/components/quick-actions/quick-actions.vue @@ -282,20 +282,17 @@ export default { this.taskSearchTimeout = null } - this.taskSearchTimeout = setTimeout(() => { - this.taskService.getAll({}, {s: query}) - .then(r => { - r = 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) - if (list !== null) { - t.title = `${t.title} (${list.title})` - } + 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) + if (list !== null) { + t.title = `${t.title} (${list.title})` + } - return t - }) - this.foundTasks = r - }) + return t + }) }, 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 => { - t.title = t.name - return t - }) - this.foundTeams = r - }) + this.teamSearchTimeout = setTimeout(async () => { + const r = await this.teamService.getAll({}, {s: query}) + this.foundTeams = r.map(t => { + t.title = t.name + return t + }) }, 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.closeQuickActions() - }) - .catch((e) => { - this.$message.error(e) - }) + this.$message.success({message: this.$t('task.createSuccess')}) + this.$router.push({name: 'task.detail', params: {id: task.id}}) + this.closeQuickActions() }, - 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 => { - this.$message.success({message: this.$t('list.create.createdSuccess')}) - this.$router.push({name: 'list.index', params: {listId: r.id}}) - this.closeQuickActions() - }) - .catch((e) => { - this.$message.error(e) - }) + 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: list.id}}) + this.closeQuickActions() }, - newNamespace() { + async newNamespace() { const newNamespace = new NamespaceModel({title: this.query}) - this.$store.dispatch('namespaces/createNamespace', newNamespace) - .then(() => { - this.$message.success({message: this.$t('namespace.create.success')}) - this.closeQuickActions() - }) - .catch((e) => { - this.$message.error(e) - }) + await this.$store.dispatch('namespaces/createNamespace', newNamespace) + this.$message.success({message: this.$t('namespace.create.success')}) + this.closeQuickActions() }, - newTeam() { + async newTeam() { const newTeam = new TeamModel({name: this.query}) - this.teamService.create(newTeam) - .then(r => { - this.$router.push({ - name: 'teams.edit', - params: {id: r.id}, - }) - this.$message.success({message: this.$t('team.create.success')}) - this.closeQuickActions() - }) - .catch((e) => { - this.$message.error(e) - }) + const team = await this.teamService.create(newTeam) + this.$router.push({ + name: 'teams.edit', + params: {id: team.id}, + }) + this.$message.success({message: this.$t('team.create.success')}) + this.closeQuickActions() }, select(parentIndex, index) { diff --git a/src/components/sharing/linkSharing.vue b/src/components/sharing/linkSharing.vue index cbebea28..6b0dee34 100644 --- a/src/components/sharing/linkSharing.vue +++ b/src/components/sharing/linkSharing.vue @@ -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(() => { - 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.linkShareService.create(newLinkShare) + this.selectedRight = rights.READ + this.name = '' + this.password = '' + this.showNewForm = false + this.$message.success({message: this.$t('list.share.links.createSuccess')}) + await this.load(listId) }, - remove(listId) { + async remove(listId) { const linkshare = new LinkShareModel({ id: this.linkIdToDelete, listId, }) - this.linkShareService - .delete(linkshare) - .then(() => { - this.$message.success({message: this.$t('list.share.links.deleteSuccess')}) - this.load(listId) - }) - .catch((e) => { - this.$message.error(e) - }) - .finally(() => { - this.showDeleteModal = false - }) + try { + await this.linkShareService.delete(linkshare) + this.$message.success({message: this.$t('list.share.links.deleteSuccess')}) + await this.load(listId) + } finally { + this.showDeleteModal = false + } }, copy, getShareLink(hash) { diff --git a/src/components/sharing/userTeam.vue b/src/components/sharing/userTeam.vue index 66bd524f..f6b7cb13 100644 --- a/src/components/sharing/userTeam.vue +++ b/src/components/sharing/userTeam.vue @@ -272,45 +272,34 @@ export default { this.load() }, methods: { - load() { - this.stuffService - .getAll(this.stuffModel) - .then((r) => { - this.sharables = r - r.forEach((s) => - this.selectedRight[s.id] = s.right, - ) - }) - .catch((e) => { - this.$message.error(e) - }) + async load() { + this.sharables = await this.stuffService.getAll(this.stuffModel) + this.sharables.forEach((s) => + this.selectedRight[s.id] = s.right, + ) }, - 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(() => { - this.showDeleteModal = false - for (const i in this.sharables) { - if ( - (this.sharables[i].username === this.stuffModel.userId && this.shareType === 'user') || - (this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team') - ) { - this.sharables.splice(i, 1) - } - } - this.$message.success({message: this.$t('list.share.userTeam.removeSuccess', {type: this.shareTypeName, sharable: this.sharableName})}) - }) - .catch((e) => { - this.$message.error(e) - }) + await this.stuffService.delete(this.stuffModel) + this.showDeleteModal = false + for (const i in this.sharables) { + if ( + (this.sharables[i].username === this.stuffModel.userId && this.shareType === 'user') || + (this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team') + ) { + this.sharables.splice(i, 1) + } + } + this.$message.success({message: this.$t('list.share.userTeam.removeSuccess', {type: this.shareTypeName, sharable: this.sharableName})}) }, - 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(() => { - this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})}) - this.load() - }) - .catch((e) => { - this.$message.error(e) - }) + await this.stuffService.create(this.stuffModel) + this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})}) + await this.load() }, - toggleType(sharable) { + + async toggleType(sharable) { if ( this.selectedRight[sharable.id] !== rights.ADMIN && this.selectedRight[sharable.id] !== rights.READ && @@ -351,41 +335,30 @@ export default { this.stuffModel.teamId = sharable.id } - this.stuffService - .update(this.stuffModel) - .then((r) => { - for (const i in this.sharables) { - if ( - (this.sharables[i].username === - this.stuffModel.userId && - this.shareType === 'user') || - (this.sharables[i].id === this.stuffModel.teamId && - this.shareType === 'team') - ) { - this.sharables[i].right = r.right - } - } - this.$message.success({message: this.$t('list.share.userTeam.updatedSuccess', {type: this.shareTypeName})}) - }) - .catch((e) => { - this.$message.error(e) - }) + const r = await this.stuffService.update(this.stuffModel) + for (const i in this.sharables) { + if ( + (this.sharables[i].username === + this.stuffModel.userId && + this.shareType === 'user') || + (this.sharables[i].id === this.stuffModel.teamId && + this.shareType === 'team') + ) { + this.sharables[i].right = r.right + } + } + this.$message.success({message: this.$t('list.share.userTeam.updatedSuccess', {type: this.shareTypeName})}) }, - 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 = [] }, diff --git a/src/components/tasks/add-task.vue b/src/components/tasks/add-task.vue index ade0ea76..3ae8d4f8 100644 --- a/src/components/tasks/add-task.vue +++ b/src/components/tasks/add-task.vue @@ -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', { - title: this.newTaskTitle, - listId: this.$store.state.auth.settings.defaultListId, - position: this.defaultPosition, - }) - .then(task => { - this.$emit('taskAdded', task) - return task - }), - ) + const task = await this.$store.dispatch('tasks/createNewTask', { + title: this.newTaskTitle, + listId: this.$store.state.auth.settings.defaultListId, + position: this.defaultPosition, + }) + this.$emit('taskAdded', task) + return task }) - Promise.all(newTasks) - .then(() => { - this.newTaskTitle = '' - }) - .catch(e => { - if (e === 'NO_LIST') { - this.errorMessage = this.$t('list.create.addListRequired') - return - } - this.$message.error(e) - }) + try { + await Promise.all(newTasks) + this.newTaskTitle = '' + } catch(e) { + if (e.message === 'NO_LIST') { + this.errorMessage = this.$t('list.create.addListRequired') + return + } + throw e + } }, handleEnter(e) { // when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create diff --git a/src/components/tasks/edit-task.vue b/src/components/tasks/edit-task.vue index dc92cfba..53459736 100644 --- a/src/components/tasks/edit-task.vue +++ b/src/components/tasks/edit-task.vue @@ -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 - this.initTaskFields() - this.$message.success({message: this.$t('task.detail.updateSuccess')}) - }) - .catch((e) => { - this.$message.error(e) - }) + async editTaskSubmit() { + this.taskEditTask = await this.taskService.update(this.taskEditTask) + this.initTaskFields() + this.$message.success({message: this.$t('task.detail.updateSuccess')}) }, }, } diff --git a/src/components/tasks/gantt-component.vue b/src/components/tasks/gantt-component.vue index 2ea3591f..8981ac10 100644 --- a/src/components/tasks/gantt-component.vue +++ b/src/components/tasks/gantt-component.vue @@ -297,54 +297,41 @@ 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) => { - if (page < this.taskCollectionService.totalPages) { - return getAllTasks(page + 1).then((nextTasks) => { - return tasks.concat(nextTasks) - }) - } else { - return tasks - } - }) - .catch((e) => { - return Promise.reject(e) - }) + const getAllTasks = async (page = 1) => { + const tasks = await this.taskCollectionService.getAll({listId: this.listId}, this.params, page) + if (page < this.taskCollectionService.totalPages) { + const nextTasks = await getAllTasks(page + 1) + return tasks.concat(nextTasks) + } + return tasks } - getAllTasks() - .then((tasks) => { - this.theTasks = tasks - .filter((t) => { - if (t.startDate === null && !t.done) { - this.tasksWithoutDates.push(t) - } - return ( - t.startDate >= this.startDate && - t.endDate <= this.endDate - ) - }) - .map((t) => { - return this.addGantAttributes(t) - }) - .sort(function (a, b) { - if (a.startDate < b.startDate) return -1 - if (a.startDate > b.startDate) return 1 - return 0 - }) + const tasks = await getAllTasks() + this.theTasks = tasks + .filter((t) => { + if (t.startDate === null && !t.done) { + this.tasksWithoutDates.push(t) + } + return ( + t.startDate >= this.startDate && + t.endDate <= this.endDate + ) }) - .catch((e) => { - this.$message.error(e) + .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 }) }, addGantAttributes(t) { @@ -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,34 +385,28 @@ export default { offsetDays: newTask.offsetDays, } - this.taskService - .update(newTask) - .then(r => { - r.endDate = ganttData.endDate - r.durationDays = ganttData.durationDays - r.offsetDays = ganttData.offsetDays + const r = await this.taskService.update(newTask) + r.endDate = ganttData.endDate + r.durationDays = ganttData.durationDays + r.offsetDays = ganttData.offsetDays - // If the task didn't have dates before, we'll update the list - if (didntHaveDates) { - for (const t in this.tasksWithoutDates) { - if (this.tasksWithoutDates[t].id === r.id) { - this.tasksWithoutDates.splice(t, 1) - break - } - } - this.theTasks.push(this.addGantAttributes(r)) - } else { - for (const tt in this.theTasks) { - if (this.theTasks[tt].id === r.id) { - this.theTasks[tt] = this.addGantAttributes(r) - break - } - } + // If the task didn't have dates before, we'll update the list + if (didntHaveDates) { + for (const t in this.tasksWithoutDates) { + if (this.tasksWithoutDates[t].id === r.id) { + this.tasksWithoutDates.splice(t, 1) + break } - }) - .catch((e) => { - this.$message.error(e) - }) + } + this.theTasks.push(this.addGantAttributes(r)) + } else { + for (const tt in this.theTasks) { + if (this.theTasks[tt].id === r.id) { + this.theTasks[tt] = this.addGantAttributes(r) + break + } + } + } }, 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) => { - this.tasksWithoutDates.push(this.addGantAttributes(r)) - this.newTaskTitle = '' - this.hideCrateNewTask() - }) - .catch((e) => { - this.$message.error(e) - }) + const r = await this.taskService.create(task) + this.tasksWithoutDates.push(this.addGantAttributes(r)) + this.newTaskTitle = '' + this.hideCrateNewTask() }, formatYear(date) { return this.format(date, 'MMMM, yyyy') diff --git a/src/components/tasks/mixins/taskList.js b/src/components/tasks/mixins/taskList.js index 130e9390..d411e78f 100644 --- a/src/components/tasks/mixins/taskList.js +++ b/src/components/tasks/mixins/taskList.js @@ -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.currentPage = page - - this.loadedList = JSON.parse(JSON.stringify(currentList)) - }) - .catch(e => { - this.$message.error(e) - }) + this.tasks = await this.taskCollectionService.getAll(list, params, page) + this.currentPage = page + this.loadedList = JSON.parse(JSON.stringify(currentList)) }, loadTasksForPage(e) { diff --git a/src/components/tasks/partials/attachments.vue b/src/components/tasks/partials/attachments.vue index a641a819..ba03e7b2 100644 --- a/src/components/tasks/partials/attachments.vue +++ b/src/components/tasks/partials/attachments.vue @@ -218,24 +218,19 @@ export default { uploadFiles(files) { uploadFiles(this.attachmentService, this.taskId, files) }, - deleteAttachment() { - this.attachmentService - .delete(this.attachmentToDelete) - .then((r) => { - this.$store.commit( - 'attachments/removeById', - this.attachmentToDelete.id, - ) - this.$message.success(r) - }) - .catch((e) => { - this.$message.error(e) - }) - .finally(() => { - this.showDeleteModal = false - }) + async deleteAttachment() { + try { + const r = await this.attachmentService.delete(this.attachmentToDelete) + this.$store.commit( + 'attachments/removeById', + this.attachmentToDelete.id, + ) + this.$message.success(r) + } 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) } diff --git a/src/components/tasks/partials/comments.vue b/src/components/tasks/partials/comments.vue index b217ea10..73accf97 100644 --- a/src/components/tasks/partials/comments.vue +++ b/src/components/tasks/partials/comments.vue @@ -134,9 +134,9 @@ @@ -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({ + userAvatar: state => state.auth.info.getAvatarUrl(48), + enabled: state => state.config.taskCommentsEnabled, + }), + actions() { + if (!this.canWrite) { + return {} + } + return Object.fromEntries(this.comments.map((c) => ([ + c.id, + [{ + action: () => this.toggleDelete(c.id), + title: this.$t('misc.delete'), + }], + ]))) }, }, - computed: mapState({ - userAvatar: state => state.auth.info.getAvatarUrl(48), - enabled: state => state.config.taskCommentsEnabled, - }), + methods: { 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) - this.newComment.comment = '' - this.$message.success({message: this.$t('task.comment.addedSuccess')}) - this.makeActions() - }) - .catch((e) => { - this.$message.error(e) - }) - .finally(() => { - this.creating = false - }) + 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')}) + } 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) => { - for (const c in this.comments) { - if (this.comments[c].id === this.commentEdit.id) { - this.comments[c] = 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] = comment } - this.saved = this.commentEdit.id - setTimeout(() => { - this.saved = null - }, 2000) - }) - .catch((e) => { - this.$message.error(e) - }) - .finally(() => { - this.isCommentEdit = false - this.saving = null - }) + } + this.saved = this.commentEdit.id + setTimeout(() => { + this.saved = null + }, 2000) + } 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(() => { - 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'), - }, - ] - }) + + 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 } }, }, diff --git a/src/components/tasks/partials/defer-task.vue b/src/components/tasks/partials/defer-task.vue index 2e66b501..c5ef510b 100644 --- a/src/components/tasks/partials/defer-task.vue +++ b/src/components/tasks/partials/defer-task.vue @@ -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) }, }, } diff --git a/src/components/tasks/partials/description.vue b/src/components/tasks/partials/description.vue index fe721158..28fd5a88 100644 --- a/src/components/tasks/partials/description.vue +++ b/src/components/tasks/partials/description.vue @@ -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) - this.saved = true - setTimeout(() => { - this.saved = false - }, 2000) - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.saving = false - }) + 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) + } finally { + this.saving = false + } }, }, } diff --git a/src/components/tasks/partials/editAssignees.vue b/src/components/tasks/partials/editAssignees.vue index 17ecc0db..adac63c7 100644 --- a/src/components/tasks/partials/editAssignees.vue +++ b/src/components/tasks/partials/editAssignees.vue @@ -78,49 +78,40 @@ export default { }, }, methods: { - addAssignee(user) { - this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId}) - .then(() => { - this.$emit('update:modelValue', this.assignees) - this.$message.success({message: this.$t('task.assignee.assignSuccess')}) - }) - .catch(e => { - this.$message.error(e) - }) + 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')}) }, - removeAssignee(user) { - this.$store.dispatch('tasks/removeAssignee', {user: user, taskId: this.taskId}) - .then(() => { - // Remove the assignee from the list - for (const a in this.assignees) { - if (this.assignees[a].id === user.id) { - this.assignees.splice(a, 1) - } - } - this.$message.success({message: this.$t('task.assignee.unassignSuccess')}) - }) - .catch(e => { - this.$message.error(e) - }) + + 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) { + this.assignees.splice(a, 1) + } + } + this.$message.success({message: this.$t('task.assignee.unassignSuccess')}) }, - findUser(query) { + + async findUser(query) { if (query === '') { this.clearAllFoundUsers() return } - this.listUserService.getAll({listId: this.listId}, {s: query}) - .then(response => { - // 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) - }) + 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)) }, + clearAllFoundUsers() { this.foundUsers = [] }, + focus() { this.$refs.multiselect.focus() }, diff --git a/src/components/tasks/partials/editLabels.vue b/src/components/tasks/partials/editLabels.vue index 41cf01c0..25ee20b6 100644 --- a/src/components/tasks/partials/editLabels.vue +++ b/src/components/tasks/partials/editLabels.vue @@ -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(() => { - bubble() - if (showNotification) { - this.$message.success({message: this.$t('task.label.addSuccess')}) - } - }) - .catch(e => { - this.$message.error(e) - }) + await this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId}) + bubble() + if (showNotification) { + this.$message.success({message: this.$t('task.label.addSuccess')}) + } }, - 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(() => { - removeFromState() - this.$message.success({message: this.$t('task.label.removeSuccess')}) - }) - .catch(e => { - this.$message.error(e) - }) + await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId}) + removeFromState() + this.$message.success({message: this.$t('task.label.removeSuccess')}) }, - 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) - this.$message.success({message: this.$t('task.label.addCreateSuccess')}) - }) - .catch(e => { - this.$message.error(e) - }) + 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')}) }, }, diff --git a/src/components/tasks/partials/heading.vue b/src/components/tasks/partials/heading.vue index 55f86f58..7ba4d1d8 100644 --- a/src/components/tasks/partials/heading.vue +++ b/src/components/tasks/partials/heading.vue @@ -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) => { - this.$emit('update:modelValue', task) - this.showSavedMessage = true - setTimeout(() => { - this.showSavedMessage = false - }, 2000) - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.saving = false - }) + try { + const task = await this.$store.dispatch('tasks/update', newTask) + this.$emit('update:modelValue', task) + this.showSavedMessage = true + setTimeout(() => { + this.showSavedMessage = false + }, 2000) + } + finally { + this.saving = false + } }, }, } diff --git a/src/components/tasks/partials/kanban-card.vue b/src/components/tasks/partials/kanban-card.vue index e7e30694..6bc60297 100644 --- a/src/components/tasks/partials/kanban-card.vue +++ b/src/components/tasks/partials/kanban-card.vue @@ -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" > @@ -93,23 +93,19 @@ export default { }, }, methods: { - markTaskAsDone(task) { + async toggleTaskDone(task) { this.loadingInternal = true - this.$store.dispatch('tasks/update', { - ...task, - done: !task.done, - }) - .then(() => { - if (task.done) { - playPop() - } - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.loadingInternal = false + try { + await this.$store.dispatch('tasks/update', { + ...task, + done: !task.done, }) + if (task.done) { + playPop() + } + } finally { + this.loadingInternal = false + } }, }, } diff --git a/src/components/tasks/partials/listSearch.vue b/src/components/tasks/partials/listSearch.vue index 14f61b64..252424f4 100644 --- a/src/components/tasks/partials/listSearch.vue +++ b/src/components/tasks/partials/listSearch.vue @@ -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) { diff --git a/src/components/tasks/partials/relatedTasks.vue b/src/components/tasks/partials/relatedTasks.vue index dad4072e..9279e147 100644 --- a/src/components/tasks/partials/relatedTasks.vue +++ b/src/components/tasks/partials/relatedTasks.vue @@ -185,76 +185,62 @@ 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(() => { - if (!this.relatedTasks[this.newTaskRelationKind]) { - this.relatedTasks[this.newTaskRelationKind] = [] - } - this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask) - this.newTaskRelationTask = null - this.saved = true - this.showNewRelationForm = false - setTimeout(() => { - this.saved = false - }, 2000) - }) - .catch(e => { - this.$message.error(e) - }) + await this.taskRelationService.create(rel) + if (!this.relatedTasks[this.newTaskRelationKind]) { + this.relatedTasks[this.newTaskRelationKind] = [] + } + this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask) + this.newTaskRelationTask = null + this.saved = true + this.showNewRelationForm = false + setTimeout(() => { + this.saved = false + }, 2000) }, - 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) { - this.relatedTasks[relationKind].splice(t, 1) - } - } - }) - this.saved = true - setTimeout(() => { - this.saved = false - }, 2000) - }) - .catch(e => { - this.$message.error(e) - }) - .finally(() => { - this.showDeleteModal = false + 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) + } 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) }, diff --git a/src/components/tasks/partials/singleTaskInList.vue b/src/components/tasks/partials/singleTaskInList.vue index d2d21129..697207cf 100644 --- a/src/components/tasks/partials/singleTaskInList.vue +++ b/src/components/tasks/partials/singleTaskInList.vue @@ -166,56 +166,47 @@ export default { }, }, methods: { - markAsDone(checked) { - const updateFunc = () => { - this.taskService.update(this.task) - .then(t => { - if (this.task.done) { - playPop() - } - this.task = t - this.$emit('task-updated', t) - this.$message.success({ - message: this.task.done ? - this.$t('task.doneSuccess') : - this.$t('task.undoneSuccess'), - }, [{ - title: 'Undo', - callback: () => { - this.task.done = !this.task.done - this.markAsDone(!checked) - }, - }]) - }) - .catch(e => { - this.$message.error(e) - }) + async markAsDone(checked) { + const updateFunc = async () => { + const task = await this.taskService.update(this.task) + if (this.task.done) { + playPop() + } + 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() { + this.task.done = !this.task.done + this.markAsDone(!checked) + }, + }]) } 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.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist') - }) - .catch(e => { - this.$message.error(e) - }) + this.task = await this.taskService.update(this.task) + this.$emit('task-updated', this.task) + this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist') }, hideDeferDueDatePopup(e) { - if (this.showDefer) { - closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => { - this.showDefer = false - }) + if (!this.showDefer) { + return } + closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => { + this.showDefer = false + }) }, }, } diff --git a/src/components/user/avatar-settings.vue b/src/components/user/avatar-settings.vue index 8fb03565..6d42f17d 100644 --- a/src/components/user/avatar-settings.vue +++ b/src/components/user/avatar-settings.vue @@ -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(() => { - this.$message.success({message: this.$t('user.settings.avatar.statusUpdateSuccess')}) - this.$store.commit('auth/reloadAvatar') - }) - .catch(e => this.$message.error(e)) + await this.avatarService.update(avatarStatus) + this.$message.success({message: this.$t('user.settings.avatar.statusUpdateSuccess')}) + this.$store.commit('auth/reloadAvatar') }, - uploadAvatar() { + + async uploadAvatar() { this.loading = true const {canvas} = this.$refs.cropper.getResult() - if (canvas) { - canvas.toBlob(blob => { - this.avatarService.create(blob) - .then(() => { - this.$message.success({message: this.$t('user.settings.avatar.setSuccess')}) - this.$store.commit('auth/reloadAvatar') - }) - .catch(e => this.$message.error(e)) - .finally(() => { - this.loading = false - this.isCropAvatar = false - }) - }) - } else { + 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') + } finally { + this.loading = false + this.isCropAvatar = false } }, + cropAvatar() { const avatar = this.$refs.avatarUploadInput.files diff --git a/src/components/user/settings/data-export.vue b/src/components/user/settings/data-export.vue index 3060d038..f092444f 100644 --- a/src/components/user/settings/data-export.vue +++ b/src/components/user/settings/data-export.vue @@ -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(() => { - this.$message.success({message: this.$t('user.export.success')}) - this.password = '' - }) - .catch(e => this.$message.error(e)) + await this.dataExportService.request(this.password) + this.$message.success({message: this.$t('user.export.success')}) + this.password = '' }, }, } diff --git a/src/components/user/settings/deletion.vue b/src/components/user/settings/deletion.vue index a769d737..f51c7909 100644 --- a/src/components/user/settings/deletion.vue +++ b/src/components/user/settings/deletion.vue @@ -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(() => { - this.$message.success({message: this.$t('user.deletion.requestSuccess')}) - this.password = '' - }) - .catch(e => this.$message.error(e)) + await this.accountDeleteService.request(this.password) + this.$message.success({message: this.$t('user.deletion.requestSuccess')}) + this.password = '' }, - cancelDeletion() { + + async cancelDeletion() { if (this.password === '') { this.errPasswordRequired = true this.$refs.passwordInput.focus() return } - this.accountDeleteService.cancel(this.password) - .then(() => { - this.$message.success({message: this.$t('user.deletion.scheduledCancelSuccess')}) - this.$store.dispatch('auth/refreshUserInfo') - this.password = '' - }) - .catch(e => this.$message.error(e)) + await this.accountDeleteService.cancel(this.password) + this.$message.success({message: this.$t('user.deletion.scheduledCancelSuccess')}) + this.$store.dispatch('auth/refreshUserInfo') + this.password = '' }, }, } diff --git a/src/helpers/attachments.ts b/src/helpers/attachments.ts index 09ec77f9..4532bee1 100644 --- a/src/helpers/attachments.ts +++ b/src/helpers/attachments.ts @@ -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) => { - store.dispatch('tasks/addTaskAttachment', { - taskId, - attachment, - }) - onSuccess(generateAttachmentUrl(taskId, attachment.id)) - }) - } - if (r.errors !== null) { - throw Error(r.errors) - } + 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) + } } export function generateAttachmentUrl(taskId: number, attachmentId: number) : any { diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts index 53444ff1..b073a8ca 100644 --- a/src/helpers/auth.ts +++ b/src/helpers/auth.ts @@ -41,21 +41,19 @@ export const removeToken = () => { * Refreshes an auth token while ensuring it is updated everywhere. * @returns {Promise>} */ -export const refreshToken = (persist: boolean): Promise => { +export async function refreshToken(persist: boolean): Promise { const HTTP = HTTPFactory() - return 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) + try { + const response = await HTTP.post('user/token', null, { + headers: { + Authorization: `Bearer ${getToken()}`, + }, }) + saveToken(response.data.token, persist) + return response + + } catch(e) { + throw new Error('Error renewing token: ', { cause: e }) + } } diff --git a/src/helpers/labels.ts b/src/helpers/labels.ts index c38a6261..48183d46 100644 --- a/src/helpers/labels.ts +++ b/src/helpers/labels.ts @@ -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)) +} \ No newline at end of file diff --git a/src/helpers/migrator.ts b/src/helpers/migrator.ts index 7949ab69..92106b76 100644 --- a/src/helpers/migrator.ts +++ b/src/helpers/migrator.ts @@ -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) } } diff --git a/src/i18n/index.js b/src/i18n/index.js index 60ccde6a..0f787d7d 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -27,14 +27,13 @@ const setI18nLanguage = lang => { } export const loadLanguageAsync = lang => { - // If the same language - if (i18n.global.locale === lang) { - return Promise.resolve(setI18nLanguage(lang)) - } - - // If the language was already loaded - if (loadedLanguages.includes(lang)) { - return Promise.resolve(setI18nLanguage(lang)) + if ( + // If the same language + i18n.global.locale === lang || + // If the language was already loaded + loadedLanguages.includes(lang) + ) { + return setI18nLanguage(lang) } // If the language hasn't been loaded yet diff --git a/src/main.ts b/src/main.ts index 179446a0..d92234ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, diff --git a/src/models/task.js b/src/models/task.js index 79c0a8e0..2b3a9f7c 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -24,16 +24,12 @@ 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) - // Every time we see a reminder, we schedule a notification for it - this.scheduleNotification(d) - return d - }) - }) + this.cancelScheduledNotifications().then(() => { + // Every time we see a reminder, we schedule a notification for it + this.reminderDates.forEach(d => this.scheduleNotification(d)) + }) // Parse the repeat after into something usable this.parseRepeatAfter() @@ -218,27 +214,26 @@ export default class TaskModel extends AbstractModel { } // Register the actual notification - 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, - // eslint-disable-next-line no-undef - showTrigger: new TimestampTrigger(date), - badge: '/images/icons/badge-monochrome.png', - icon: '/images/icons/android-chrome-512x512.png', - data: {taskId: this.id}, - actions: [ - { - action: 'show-task', - title: 'Show task', - }, - ], - }) - .then(() => { - console.debug('Notification scheduled for ' + date) - }) - .catch(e => { - console.debug('Error scheduling notification', e) + 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, + // eslint-disable-next-line no-undef + showTrigger: new TimestampTrigger(date), + badge: '/images/icons/badge-monochrome.png', + icon: '/images/icons/android-chrome-512x512.png', + data: {taskId: this.id}, + actions: [ + { + action: 'show-task', + title: 'Show task', + }, + ], }) + console.debug('Notification scheduled for ' + date) + } catch(e) { + throw new Error('Error scheduling notification', e) + } } } diff --git a/src/services/abstractService.js b/src/services/abstractService.js index 0aef3c37..aa8e7b45 100644 --- a/src/services/abstractService.js +++ b/src/services/abstractService.js @@ -105,19 +105,6 @@ export default class AbstractService { return true } - ///////////////////// - // Global error handler - /////////////////// - - /** - * Handles the error and rejects the promise. - * @param error - * @returns {Promise} - */ - 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} */ - 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 => { - const result = this.modelGetFactory(response.data) - result.maxRight = Number(response.headers['x-max-right']) - return Promise.resolve(result) - }) - .finally(() => { - cancel() - }) + try { + const response = await this.http.get(finalUrl, {params}) + const result = this.modelGetFactory(response.data) + result.maxRight = Number(response.headers['x-max-right']) + 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} */ - 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 => { - this.resultCount = Number(response.headers['x-pagination-result-count']) - this.totalPages = Number(response.headers['x-pagination-total-pages']) + 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 (response.data === null) { + return [] + } - if (Array.isArray(response.data)) { - return Promise.resolve(response.data.map(entry => { - return this.modelGetAllFactory(entry) - })) - } - if (response.data === null) { - return Promise.resolve([]) - } - return Promise.resolve(this.modelGetAllFactory(response.data)) - }) - .finally(() => { - cancel() - }) + 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} */ - 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 => { - const result = this.modelCreateFactory(response.data) - if (typeof model.maxRight !== 'undefined') { - result.maxRight = model.maxRight - } - return Promise.resolve(result) - }) - .finally(() => { - cancel() - }) + 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 result + } finally { + cancel() + } } /** @@ -407,23 +380,19 @@ export default class AbstractService { * @param model * @returns {Q.Promise} */ - 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 => { - const result = this.modelUpdateFactory(response.data) - if (typeof model.maxRight !== 'undefined') { - result.maxRight = model.maxRight - } - return Promise.resolve(result) - }) - .finally(() => { - cancel() - }) + 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 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} */ - 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(() => { - cancel() - }) + try { + const {data} = await this.http.delete(finalUrl, model) + return data + } finally { + cancel() + } } /** @@ -496,32 +461,28 @@ export default class AbstractService { * @param formData * @returns {Q.Promise} */ - uploadFormData(url, formData) { + async uploadFormData(url, formData) { console.log(formData, formData._boundary) const cancel = this.setLoading() - return this.http.put( - url, - formData, - { - headers: { - 'Content-Type': + try { + const response = await this.http.put( + url, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + formData._boundary, + }, + onUploadProgress: progressEvent => { + this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total) + }, }, - onUploadProgress: progressEvent => { - this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total) - }, - }, - ) - .catch(error => { - return this.errorHandler(error) - }) - .then(response => { - return Promise.resolve(this.modelCreateFactory(response.data)) - }) - .finally(() => { - this.uploadProgress = 0 - cancel() - }) + ) + this.modelCreateFactory(response.data) + } finally { + this.uploadProgress = 0 + cancel() + } } } \ No newline at end of file diff --git a/src/services/attachment.js b/src/services/attachment.js index afd28cdc..098d5ef6 100644 --- a/src/services/attachment.js +++ b/src/services/attachment.js @@ -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) } /** diff --git a/src/services/backgroundUnsplash.js b/src/services/backgroundUnsplash.js index f0d29645..09b748ac 100644 --- a/src/services/backgroundUnsplash.js +++ b/src/services/backgroundUnsplash.js @@ -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 - }) + return window.URL.createObjectURL(new Blob([response.data])) } } \ No newline at end of file diff --git a/src/services/dataExport.js b/src/services/dataExport.js index edadf756..9f8720bf 100644 --- a/src/services/dataExport.js +++ b/src/services/dataExport.js @@ -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() + } } } \ No newline at end of file diff --git a/src/services/list.js b/src/services/list.js index 5fd2fc3d..3d61218f 100644 --- a/src/services/list.js +++ b/src/services/list.js @@ -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 - }) + return window.URL.createObjectURL(new Blob([response.data])) } - 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(() => { - cancel() - }) + try { + const response = await this.http.delete(`/lists/${list.id}/background`, list) + return response.data + } finally { + cancel() + } } } \ No newline at end of file diff --git a/src/services/passwordReset.js b/src/services/passwordReset.js index 6f972504..9d3dc56b 100644 --- a/src/services/passwordReset.js +++ b/src/services/passwordReset.js @@ -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(() => { - cancel() - }) + 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(() => { - cancel() - }) + try { + const response = await this.http.post(this.paths.requestReset, model) + return this.modelFactory(response.data) + } finally { + cancel() + } } } \ No newline at end of file diff --git a/src/services/totp.js b/src/services/totp.js index 021d68e0..93367ab3 100644 --- a/src/services/totp.js +++ b/src/services/totp.js @@ -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]) } } \ No newline at end of file diff --git a/src/store/modules/auth.js b/src/store/modules/auth.js index f9e5b159..2d5bbec8 100644 --- a/src/store/modules/auth.js +++ b/src/store/modules/auth.js @@ -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 => { - // Save the token to local storage for later use - saveToken(response.data.token, true) + 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') + } catch(e) { + if ( + e.response && + e.response.data.code === 1017 && + !credentials.totpPasscode + ) { + ctx.commit('needsTotpPasscode', 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) { - ctx.commit('needsTotpPasscode', true) - return Promise.reject(e) - } - } - - return Promise.reject(e) - }) - .finally(() => { - ctx.commit(LOADING, false, {root: true}) - }) + 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', { - username: credentials.username, - email: credentials.email, - password: credentials.password, - }) - .then(() => { - return ctx.dispatch('login', credentials) + try { + await HTTP.post('register', { + username: credentials.username, + email: credentials.email, + password: credentials.password, }) - .catch(e => { - if (e.response && e.response.data && e.response.data.message) { - ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true}) - } + return ctx.dispatch('login', credentials) + } 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(() => { - ctx.commit(LOADING, false, {root: true}) - }) + 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 => { - // 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(() => { - ctx.commit(LOADING, false, {root: true}) - }) + 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') + } 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) - ctx.dispatch('checkAuth') - return Promise.resolve(r.data) - }).catch(e => { - return Promise.reject(e) - }) + saveToken(response.data.token, false) + ctx.dispatch('checkAuth') + 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', { - headers: { - Authorization: `Bearer ${jwt}`, - }, - }) - .then(r => { - const info = new UserModel(r.data) - info.type = ctx.state.info.type - info.email = ctx.state.info.email - info.exp = ctx.state.info.exp + try { - ctx.commit('info', info) - ctx.commit('lastUserRefresh') - }) - .catch(e => { - console.error('Error while refreshing user info:', e) + const response = await HTTP.get('user', { + headers: { + Authorization: `Bearer ${jwt}`, + }, }) + const info = new UserModel(response.data) + info.type = ctx.state.info.type + info.email = ctx.state.info.email + info.exp = ctx.state.info.exp + + ctx.commit('info', info) + ctx.commit('lastUserRefresh') + return info + } catch(e) { + throw new Error('Error while refreshing user info:', { cause: e }) + } }, + // Renews the api token and saves it to local storage 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(() => { - ctx.dispatch('checkAuth') - }) - .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') - } - }) + try { + await refreshToken(!ctx.state.isLinkShareAuth) + ctx.dispatch('checkAuth') + } 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) { diff --git a/src/store/modules/config.js b/src/store/modules/config.js index 5019d901..e28a6b46 100644 --- a/src/store/modules/config.js +++ b/src/store/modules/config.js @@ -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 && diff --git a/src/store/modules/kanban.js b/src/store/modules/kanban.js index cf7413d3..acd6824e 100644 --- a/src/store/modules/kanban.js +++ b/src/store/modules/kanban.js @@ -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) - ctx.commit('setListId', listId) - return Promise.resolve(r) - }) - .catch(e => { - return Promise.reject(e) - }) - .finally(() => { - cancel() - }) + try { + const response = await bucketService.getAll({listId: listId}, params) + ctx.commit('setBuckets', response) + ctx.commit('setListId', listId) + 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}) - ctx.commit('setTasksLoadedForBucketPage', {bucketId, page}) - if (taskService.totalPages <= page) { - ctx.commit('setAllTasksLoadedForBucket', bucketId) - } - return Promise.resolve(r) - }) - .catch(e => { - return Promise.reject(e) - }) - .finally(() => { - cancel() - ctx.commit('setBucketLoading', {bucketId: bucketId, loading: false}) - }) + 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 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(() => { - cancel() - }) + 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 => { - 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(() => { - cancel() - }) + 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 response + } finally { + cancel() + } }, - updateBucket(ctx, updatedBucketData) { + async updateBucket(ctx, updatedBucketData) { const cancel = setLoading(ctx, 'kanban') const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id) @@ -342,22 +327,22 @@ 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 => { - // restore original state - ctx.commit('setBucketByIndex', {bucketIndex, bucket: oldBucket}) + const bucketService = new BucketService() + 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(() => { - success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) - }) + await ctx.dispatch('updateBucket', updatedBucketData) + success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) }, }, } \ No newline at end of file diff --git a/src/store/modules/labels.js b/src/store/modules/labels.js index 2851d60a..5326483b 100644 --- a/src/store/modules/labels.js +++ b/src/store/modules/labels.js @@ -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) - }) + try { + const labels = await getAllLabels() + ctx.commit('setLabels', labels) + ctx.commit('setLoaded', true) + return labels + } finally { + cancel() } - - return getAllLabels() - .then(r => { - ctx.commit('setLabels', r) - ctx.commit('setLoaded', true) - return Promise.resolve(r) - }) - .catch(e => Promise.reject(e)) - .finally(() => cancel()) }, - deleteLabel(ctx, label) { + async deleteLabel(ctx, label) { const cancel = setLoading(ctx, 'labels') const labelService = new LabelService() - return labelService.delete(label) - .then(r => { - ctx.commit('removeLabelById', label) - return Promise.resolve(r) - }) - .catch(e => Promise.reject(e)) - .finally(() => cancel()) + try { + const result = await labelService.delete(label) + ctx.commit('removeLabelById', label) + 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() + } }, }, } diff --git a/src/store/modules/lists.js b/src/store/modules/lists.js index d67610de..f5aa37b7 100644 --- a/src/store/modules/lists.js +++ b/src/store/modules/lists.js @@ -42,69 +42,70 @@ 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(() => { - ctx.commit('setList', list) - ctx.commit('namespaces/setListInNamespaceById', list, {root: true}) - - // the returned list from listService.update is the same! - // in order to not validate vuex mutations we have to create a new copy - const newList = { - ...list, - namespaceId: FavoriteListsNamespace, - } - if (list.isFavorite) { - ctx.commit('namespaces/addListToNamespace', newList, {root: true}) - } else { - ctx.commit('namespaces/removeListFromNamespaceById', newList, {root: true}) - } - ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true}) - ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true}) - return Promise.resolve(newList) + try { + await listService.update(list) + ctx.commit('setList', list) + ctx.commit('namespaces/setListInNamespaceById', list, {root: true}) + + // the returned list from listService.update is the same! + // in order to not validate vuex mutations we have to create a new copy + const newList = { + ...list, + namespaceId: FavoriteListsNamespace, + } + if (list.isFavorite) { + ctx.commit('namespaces/addListToNamespace', newList, {root: true}) + } else { + ctx.commit('namespaces/removeListFromNamespaceById', newList, {root: true}) + } + ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true}) + ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true}) + return newList + } catch(e) { + // Reset the list state to the initial one to avoid confusion for the user + ctx.commit('setList', { + ...list, + isFavorite: !list.isFavorite, }) - .catch(e => { - // 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 => { - 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()) + try { + const response = await listService.delete(list) + ctx.commit('removeListById', list) + ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true}) + removeListFromHistory({id: list.id}) + return response + } finally{ + cancel() + } }, }, } \ No newline at end of file diff --git a/src/store/modules/namespaces.js b/src/store/modules/namespaces.js index 01e37ed2..cb78f560 100644 --- a/src/store/modules/namespaces.js +++ b/src/store/modules/namespaces.js @@ -94,67 +94,63 @@ export default { }, }, actions: { - loadNamespaces(ctx) { + async loadNamespaces(ctx) { const cancel = setLoading(ctx, 'namespaces') const namespaceService = new NamespaceService() - // We always load all namespaces and filter them on the frontend - return namespaceService.getAll({}, {is_archived: true}) - .then(r => { - ctx.commit('namespaces', r) - - // Put all lists in the list state - const lists = [] - r.forEach(n => { - n.lists.forEach(l => { - lists.push(l) - }) - }) - - ctx.commit('lists/setLists', lists, {root: true}) - - return Promise.resolve(r) - }) - .catch(e => Promise.reject(e)) - .finally(() => { - cancel() - }) + try { + // We always load all namespaces and filter them on the frontend + const namespaces = await namespaceService.getAll({}, {is_archived: true}) + ctx.commit('namespaces', namespaces) + + // Put all lists in the list state + const lists = namespaces.flatMap(({lists}) => lists) + + ctx.commit('lists/setLists', lists, {root: true}) + + 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 => { - ctx.commit('removeNamespaceById', namespace.id) - return Promise.resolve(r) - }) - .catch(e => Promise.reject(e)) - .finally(() => cancel()) + try { + const response = await namespaceService.delete(namespace) + ctx.commit('removeNamespaceById', namespace.id) + 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() + } }, }, } \ No newline at end of file diff --git a/src/store/modules/tasks.js b/src/store/modules/tasks.js index 618b36b5..8adf09bb 100644 --- a/src/store/modules/tasks.js +++ b/src/store/modules/tasks.js @@ -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 => { - task.labels.push(label) - return Promise.resolve(result) - }) - .catch(e => Promise.reject(e)) + const response = await labelTaskService.create(labelTask) + task.labels.push(label) + 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(() => { - cancel() - }) - + 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(() => { - cancel() - }) + 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 => { - ctx.commit('kanban/removeTaskInBucket', task, {root: true}) - return Promise.resolve(t) - }) - .catch(e => { - return Promise.reject(e) - }) + const response = await taskService.delete(task) + ctx.commit('kanban/removeTaskInBucket', task, {root: true}) + return response }, + // Adds a task attachment in store. // This is an action to be able to commit other mutations addTaskAttachment(ctx, {taskId, attachment}) { @@ -134,118 +120,97 @@ 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 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) - } - // 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) - }) + 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 r + } + // FIXME: direct store manipulation (task) + t.task.assignees.push(user) + ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true }) + 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 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) - } + 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 response + } - for (const a in t.task.assignees) { - if (t.task.assignees[a].id === user.id) { - // FIXME: direct store manipulation (task) - t.task.assignees.splice(a, 1) - break - } - } - - ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) - return Promise.resolve(r) - }) - .catch(e => { - return Promise.reject(e) - }) - - }, - - addLabel(ctx, {label, taskId}) { - const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id}) - - const labelTaskService = new LabelTaskService() - return labelTaskService.create(labelTask) - .then(r => { - const t = ctx.rootGetters['kanban/getTaskById'](taskId) - if (t.task === null) { - // Don't try further adding a label if the task is not in kanban - // Usually this means the kanban board hasn't been accessed until now. - // Vuex seems to have its difficulties with that, so we just log the error and fail silently. - console.debug('Could not add label to task in kanban, task not found', t) - return Promise.resolve(r) - } + for (const a in t.task.assignees) { + if (t.task.assignees[a].id === user.id) { // FIXME: direct store manipulation (task) - t.task.labels.push(label) - ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) + t.task.assignees.splice(a, 1) + break + } + } + + ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) + return response - return Promise.resolve(r) - }) - .catch(e => { - return Promise.reject(e) - }) }, - removeLabel(ctx, {label, taskId}) { + async addLabel(ctx, {label, taskId}) { const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id}) const labelTaskService = new LabelTaskService() - return labelTaskService.delete(labelTask) - .then(r => { - const t = ctx.rootGetters['kanban/getTaskById'](taskId) - if (t.task === null) { - // Don't try further adding a label if the task is not in kanban - // Usually this means the kanban board hasn't been accessed until now. - // Vuex seems to have its difficulties with that, so we just log the error and fail silently. - console.debug('Could not remove label from task in kanban, task not found', t) - return Promise.resolve(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 r + } + // FIXME: direct store manipulation (task) + t.task.labels.push(label) + ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true }) + return r + }, - // Remove the label from the list - for (const l in t.task.labels) { - if (t.task.labels[l].id === label.id) { - // FIXME: direct store manipulation (task) - t.task.labels.splice(l, 1) - break - } - } + async removeLabel(ctx, {label, taskId}) { + const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id}) - ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) + const labelTaskService = new LabelTaskService() + const response = await labelTaskService.delete(labelTask) + const t = ctx.rootGetters['kanban/getTaskById'](taskId) + if (t.task === null) { + // Don't try further adding a label if the task is not in kanban + // Usually this means the kanban board hasn't been accessed until now. + // Vuex seems to have its difficulties with that, so we just log the error and fail silently. + console.debug('Could not remove label from task in kanban, task not found', t) + return response + } - return Promise.resolve(r) - }) - .catch(e => { - return Promise.reject(e) - }) + // Remove the label from the list + for (const l in t.task.labels) { + if (t.task.labels[l].id === label.id) { + // FIXME: direct store manipulation (task) + t.task.labels.splice(l, 1) + break + } + } + + ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true}) + + 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, - parsedLabels: parsedTask.labels, - })) - .catch(e => Promise.reject(e)) + const createdTask = await taskService.create(task) + return dispatch('addLabelsToTask', { + task: createdTask, + parsedLabels: parsedTask.labels, + }) }, }, } \ No newline at end of file diff --git a/src/views/Home.vue b/src/views/Home.vue index 7bf6a9e6..d5231bd9 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,6 +1,6 @@