UI: fix subtask progress chip on the task list
Count a parent's subtask progress over the full task set in the repository (attached to Task as subtaskTotal/subtaskDone) so completed children are counted even on smart lists that hide closed tasks — the chip showed "0 / 2" instead of "18 / 20". List membership (and the home overview counts) are unchanged; only the parent's derived progress is. Also wrap the task-row metadata in a FlowRow so the subtask chip drops to a further line when a dated task with subtasks can't fit due + chips on one line, rather than squeezing the trailing chip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@ package de.jeanlucmakiola.floret.data.tasks
|
||||
|
||||
import de.jeanlucmakiola.floret.data.di.IoDispatcher
|
||||
import de.jeanlucmakiola.floret.domain.DayWindow
|
||||
import de.jeanlucmakiola.floret.domain.SmartList
|
||||
import de.jeanlucmakiola.floret.domain.Task
|
||||
import de.jeanlucmakiola.floret.domain.TaskDetail
|
||||
import de.jeanlucmakiola.floret.domain.TaskFilter
|
||||
@@ -44,11 +43,28 @@ class TasksRepositoryImpl @Inject constructor(
|
||||
private fun loadTasks(filter: TaskFilter): List<Task> {
|
||||
val query = when (filter) {
|
||||
is TaskFilter.OfList -> TaskQuery(listId = filter.listId, includeCompleted = true)
|
||||
is TaskFilter.Smart -> TaskQuery(includeCompleted = filter.list == SmartList.COMPLETED)
|
||||
// Read completed tasks too, then let [TaskFiltering.matches] enforce each
|
||||
// smart list's own open/closed semantics. We over-read on purpose so a
|
||||
// parent's subtask-progress reflects ALL its children — completed ones
|
||||
// included — even on smart lists that hide completed tasks themselves.
|
||||
is TaskFilter.Smart -> TaskQuery(includeCompleted = true)
|
||||
}
|
||||
val (todayStart, todayEnd) = DayWindow.today(Clock.System.now(), ZoneId.systemDefault())
|
||||
return dataSource.tasks(query)
|
||||
val all = dataSource.tasks(query)
|
||||
// (done, total) of direct children per parent, over the unfiltered set.
|
||||
val progress: Map<Long, Pair<Int, Int>> = all
|
||||
.filter { (it.parentId ?: 0L) > 0L }
|
||||
.groupingBy { it.parentId!! }
|
||||
.fold(0 to 0) { (done, total), t ->
|
||||
(if (t.isCompleted) done + 1 else done) to (total + 1)
|
||||
}
|
||||
return all
|
||||
.filter { TaskFiltering.matches(it, filter, todayStart, todayEnd) }
|
||||
.map { task ->
|
||||
progress[task.taskId]?.let { (done, total) ->
|
||||
task.copy(subtaskDone = done, subtaskTotal = total)
|
||||
} ?: task
|
||||
}
|
||||
.sortedWith(TaskSorting.DEFAULT)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ data class Task(
|
||||
val distanceFromCurrent: Int?,
|
||||
val created: Instant?,
|
||||
val lastModified: Instant?,
|
||||
/**
|
||||
* Direct-subtask progress, filled in by the repository over the *full* task
|
||||
* set so a parent's progress chip counts completed children even on smart
|
||||
* lists that hide them. Both 0 when the task has no subtasks.
|
||||
*/
|
||||
val subtaskTotal: Int = 0,
|
||||
val subtaskDone: Int = 0,
|
||||
) {
|
||||
val isCompleted: Boolean get() = status == TaskStatus.COMPLETED
|
||||
val isClosed: Boolean get() = status == TaskStatus.COMPLETED || status == TaskStatus.CANCELLED
|
||||
|
||||
@@ -10,6 +10,8 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -147,12 +149,10 @@ private fun TaskListBody(
|
||||
viewModel: TaskListViewModel,
|
||||
) {
|
||||
val (todayStart, todayEnd) = remember { DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) }
|
||||
// A parent represents its children via a progress chip, so drop children
|
||||
// from the flat list — unless their parent isn't in this view (can happen in
|
||||
// smart lists), where we keep them standalone so nothing silently vanishes.
|
||||
val childrenByParent = remember(state.tasks) {
|
||||
state.tasks.filter { (it.parentId ?: 0L) > 0L }.groupBy { it.parentId!! }
|
||||
}
|
||||
// A parent represents its children via a progress chip (counts come from the
|
||||
// repository, over the full set), so drop children from the flat list — unless
|
||||
// their parent isn't in this view (can happen in smart lists), where we keep
|
||||
// them standalone so nothing silently vanishes.
|
||||
val displayTasks = remember(state.tasks) {
|
||||
val present = state.tasks.mapTo(HashSet()) { it.taskId }
|
||||
state.tasks.filter { (it.parentId ?: 0L) <= 0L || it.parentId !in present }
|
||||
@@ -201,13 +201,12 @@ private fun TaskListBody(
|
||||
}
|
||||
if (!collapsible || completedExpanded) {
|
||||
itemsIndexed(section.tasks, key = { _, t -> t.taskId }) { index, task ->
|
||||
val children = childrenByParent[task.taskId]
|
||||
TaskRow(
|
||||
task = task,
|
||||
position = positionOf(index, section.tasks.size),
|
||||
showListName = showListName,
|
||||
subtaskDone = children?.count { it.isCompleted } ?: 0,
|
||||
subtaskTotal = children?.size ?: 0,
|
||||
subtaskDone = task.subtaskDone,
|
||||
subtaskTotal = task.subtaskTotal,
|
||||
onToggle = { viewModel.toggleComplete(task) },
|
||||
onDelete = { viewModel.delete(task) },
|
||||
onClick = { onOpenTask(task.taskId) },
|
||||
@@ -300,7 +299,7 @@ private fun TaskRow(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun TaskRowContent(
|
||||
task: Task,
|
||||
@@ -361,9 +360,13 @@ private fun TaskRowContent(
|
||||
)
|
||||
if (hasSupporting) {
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
// Flow so the chips wrap to a further line when due date + list
|
||||
// + subtask progress can't share one (e.g. a dated task with
|
||||
// subtasks), rather than squeezing the trailing chip.
|
||||
FlowRow(
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (due != null) {
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user