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)
+ })
+})