Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/components/TaskBody.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
@toggle-completed="toggleCompleted(task)" />
<!-- Info: summary, progress & tags -->
<div class="task-body__info"
@click="toggleOpenTaskSubtasks($event)"
@dblclick="editSummary()">
<div class="summary">
<span v-linkify="{text: task.summary, linkify: true}" />
Expand Down Expand Up @@ -280,6 +281,7 @@ export default {
showCreateMultipleTasksModal: false,
multipleTasks: { numberOfTasks: 0, tasks: {} },
additionalTaskProperties: {},
subtasksFoldedWhileOpen: false,
}
},
computed: {
Expand Down Expand Up @@ -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
Expand All @@ -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 })
Expand Down Expand Up @@ -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
*
Expand Down
104 changes: 104 additions & 0 deletions tests/javascript/unit/components/TaskBody.spec.js
Original file line number Diff line number Diff line change
@@ -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)
})
})