diff --git a/src/components/TaskBody.vue b/src/components/TaskBody.vue index 779a499d1..aeea6e5f6 100644 --- a/src/components/TaskBody.vue +++ b/src/components/TaskBody.vue @@ -48,6 +48,7 @@ License along with this library. If not, see . @toggle-completed="toggleCompleted(task)" />
@@ -280,6 +281,7 @@ export default { showCreateMultipleTasksModal: false, multipleTasks: { numberOfTasks: 0, tasks: {} }, additionalTaskProperties: {}, + subtasksFoldedWhileOpen: false, } }, computed: { @@ -467,7 +469,7 @@ export default { * @return {boolean} If we show the subtasks */ showSubtasks() { - if (!this.task.hideSubtasks || this.searchQuery || this.isTaskOpen() || this.isDescendantOpen()) { + if (!this.task.hideSubtasks || this.searchQuery || (this.isTaskOpen() && !this.subtasksFoldedWhileOpen) || this.isDescendantOpen()) { return true } else { return false @@ -486,6 +488,13 @@ export default { }, }, + watch: { + // Opening the task again shows its subtasks again + '$route.params.taskId'() { + this.subtasksFoldedWhileOpen = false + }, + }, + created() { if (!this.task.loadedCompleted && this.$route.params.taskId === this.task.uri) { this.getTasksFromCalendar({ calendar: this.task.calendar, completed: true, related: this.task.uid }) @@ -591,6 +600,25 @@ export default { return checkSubtasksOpen(task.subTasks) }, + /** + * Shows or hides the subtasks when the task is clicked while + * it is open in the details view + * + * @param {object} $event the click event + */ + toggleOpenTaskSubtasks($event) { + // The second click of a double click edits the summary instead + if ($event.detail > 1 || $event.target.closest('.no-nav') + || !this.isTaskOpen() || !Object.values(this.task.subTasks).length) { + return + } + const fold = this.showSubtasks + if (!!this.task.hideSubtasks !== fold) { + this.toggleSubtasksVisibility(this.task) + } + this.subtasksFoldedWhileOpen = fold + }, + /** * Navigates to a different route, but checks if navigation is desired * diff --git a/tests/javascript/unit/components/TaskBody.spec.js b/tests/javascript/unit/components/TaskBody.spec.js new file mode 100644 index 000000000..ec739c011 --- /dev/null +++ b/tests/javascript/unit/components/TaskBody.spec.js @@ -0,0 +1,104 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import TaskBody from '../../../../src/components/TaskBody.vue' +import { routes } from '../../../../src/router.js' + +import { createRouter, createWebHistory } from 'vue-router' + +import { store } from '../setupStore.js' + +import { shallowMount, flushPromises } from '@vue/test-utils' + +import { beforeEach, describe, expect, it } from 'vitest' + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +const openTask = async (taskId) => { + await router.push({ name: 'calendarsTask', params: { calendarId: 'calendar-2', taskId } }) + await flushPromises() +} + +describe('TaskBody.vue', () => { + 'use strict' + + // "Calendar 2 - Task 4", which has two subtasks + const task = store.getters.getTaskByUri('pwen8kz22g.ics') + + const mountTaskBody = () => shallowMount(TaskBody, { + global: { + plugins: [store, router], + }, + props: { + task, + }, + }) + + beforeEach(async () => { + await openTask('pwen4kz21g.ics') + task.hideSubtasks = false + }) + + it('Clicking the open task hides and shows its subtasks', async () => { + await openTask('pwen8kz22g.ics') + const wrapper = mountTaskBody() + expect(wrapper.vm.showSubtasks).toBe(true) + + await wrapper.find('.task-body__info').trigger('click') + expect(task.hideSubtasks).toBe(1) + expect(wrapper.vm.showSubtasks).toBe(false) + + await wrapper.find('.task-body__info').trigger('click') + expect(task.hideSubtasks).toBe(0) + expect(wrapper.vm.showSubtasks).toBe(true) + }) + + it('Clicking the open task hides subtasks that are only shown because it is open', async () => { + task.hideSubtasks = true + await openTask('pwen8kz22g.ics') + const wrapper = mountTaskBody() + expect(wrapper.vm.showSubtasks).toBe(true) + + await wrapper.find('.task-body__info').trigger('click') + expect(task.hideSubtasks).toBe(1) + expect(wrapper.vm.showSubtasks).toBe(false) + }) + + it('Clicking a task that is not open opens it without hiding its subtasks', async () => { + const wrapper = mountTaskBody() + + await wrapper.find('.task-body__info').trigger('click') + await flushPromises() + expect(router.currentRoute.value.params.taskId).toBe('pwen8kz22g.ics') + expect(task.hideSubtasks).toBe(0) + expect(wrapper.vm.showSubtasks).toBe(true) + }) + + it('The second click of a double click does not show the subtasks again', async () => { + await openTask('pwen8kz22g.ics') + const wrapper = mountTaskBody() + + await wrapper.find('.task-body__info').trigger('click', { detail: 1 }) + await wrapper.find('.task-body__info').trigger('click', { detail: 2 }) + expect(task.hideSubtasks).toBe(1) + expect(wrapper.vm.showSubtasks).toBe(false) + }) + + it('Opening the task again shows its subtasks again', async () => { + await openTask('pwen8kz22g.ics') + const wrapper = mountTaskBody() + + await wrapper.find('.task-body__info').trigger('click') + expect(wrapper.vm.showSubtasks).toBe(false) + + await openTask('pwen4kz21g.ics') + expect(wrapper.vm.showSubtasks).toBe(false) + await openTask('pwen8kz22g.ics') + expect(task.hideSubtasks).toBe(1) + expect(wrapper.vm.showSubtasks).toBe(true) + }) +})