From bf5be8cd20751257dc5a42f6721ae1ecf30c7c2f Mon Sep 17 00:00:00 2001
From: Raffaele Mancuso <54762742+raffaelemancuso@users.noreply.github.com>
Date: Mon, 19 Oct 2026 09:53:44 +0200
Subject: [PATCH] feat(TaskBody): toggle subtasks by clicking the open task
Clicking a task that is already open in the details view did nothing.
It now hides or shows the task's subtasks, like "Hide subtasks" and
"Show subtasks" in the actions menu, and the details view stays open.
Opening the task again shows its subtasks, as before.
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Raffaele Mancuso <54762742+raffaelemancuso@users.noreply.github.com>
---
src/components/TaskBody.vue | 30 ++++-
.../unit/components/TaskBody.spec.js | 104 ++++++++++++++++++
2 files changed, 133 insertions(+), 1 deletion(-)
create mode 100644 tests/javascript/unit/components/TaskBody.spec.js
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)
+ })
+})