Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import br.com.sailboat.todozy.feature.task.list.impl.data.repository.TaskReposit
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.CompleteTaskUseCase
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.CompleteTaskUseCaseImpl
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.GetTasksUseCaseImpl
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.MarkTaskDoneForTodayUseCase
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.MarkTaskDoneForTodayUseCaseImpl
import br.com.sailboat.todozy.feature.task.list.impl.presentation.factory.TaskListUiModelFactory
import br.com.sailboat.todozy.feature.task.list.impl.presentation.mapper.TaskCategoryToStringMapper
import br.com.sailboat.todozy.feature.task.list.impl.presentation.mapper.TaskToTaskUiModelMapper
Expand All @@ -33,6 +35,7 @@ private val presentation =
getTaskMetricsUseCase = get(),
getTaskProgressUseCase = get(),
completeTaskUseCase = get(),
markTaskDoneForTodayUseCase = get(),
taskListUiModelFactory = get(),
logService = get(),
)
Expand All @@ -43,6 +46,7 @@ private val domain =
module {
factory<GetTasksUseCase> { GetTasksUseCaseImpl(get()) }
factory<CompleteTaskUseCase> { CompleteTaskUseCaseImpl(get(), get(), get(), get(), get()) }
factory<MarkTaskDoneForTodayUseCase> { MarkTaskDoneForTodayUseCaseImpl(get(), get()) }
}

private val data =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package br.com.sailboat.todozy.feature.task.list.impl.domain.usecase

internal interface MarkTaskDoneForTodayUseCase {
suspend operator fun invoke(taskId: Long): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package br.com.sailboat.todozy.feature.task.list.impl.domain.usecase

import br.com.sailboat.todozy.domain.model.TaskStatus
import br.com.sailboat.todozy.feature.task.details.domain.usecase.GetTaskUseCase
import br.com.sailboat.todozy.feature.task.history.domain.usecase.AddHistoryUseCase

internal class MarkTaskDoneForTodayUseCaseImpl(
private val getTaskUseCase: GetTaskUseCase,
private val addHistoryUseCase: AddHistoryUseCase,
) : MarkTaskDoneForTodayUseCase {
override suspend operator fun invoke(taskId: Long): Result<Unit> {
val task = getTaskUseCase(taskId).getOrElse { throwable ->
return Result.failure(throwable)
}

addHistoryUseCase(task, TaskStatus.DONE).getOrElse { throwable ->
return Result.failure(throwable)
}

return Result.success(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ internal class TaskListFragment : Fragment() {
),
)
},
onMarkDoneForToday = { taskId ->
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
viewModel.dispatchViewIntent(TaskListViewIntent.OnMarkDoneForToday(taskId))
},
onNewTask = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
viewModel.dispatchViewIntent(TaskListViewIntent.OnClickNewTask)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.Icon
Expand Down Expand Up @@ -102,6 +104,7 @@ internal fun TaskListScreen(
onTaskClick: (Long) -> Unit,
onTaskSwipe: (taskId: Long, TaskStatus) -> Unit,
onTaskUndo: (Long, TaskStatus) -> Unit,
onMarkDoneForToday: (Long) -> Unit,
onNewTask: () -> Unit,
onSearch: (String) -> Unit,
) {
Expand All @@ -112,6 +115,7 @@ internal fun TaskListScreen(
val metricsToShow = if (tasksLoading.isTrue()) null else taskMetrics
val listState = rememberLazyListState()
var pendingScrollToTop by rememberSaveable { mutableStateOf(false) }
var contextMenuTaskId by rememberSaveable { mutableStateOf<Long?>(null) }

LaunchedEffect(tasksLoading, taskProgressLoading) {
if (tasksLoading || taskProgressLoading) {
Expand Down Expand Up @@ -191,14 +195,27 @@ internal fun TaskListScreen(
is TaskUiModel ->
SwipeableTaskItem(
task = item,
onClick = onTaskClick,
onClick = { taskId ->
contextMenuTaskId = null
onTaskClick(taskId)
},
onUndoClick = onTaskUndo,
onSwipe = { status ->
contextMenuTaskId = null
onTaskSwipe(
item.taskId,
status,
)
},
isContextMenuVisible = contextMenuTaskId == item.taskId,
onOpenContextMenu = { taskId ->
contextMenuTaskId = taskId
},
onDismissContextMenu = { contextMenuTaskId = null },
onMarkDoneForToday = { taskId ->
contextMenuTaskId = null
onMarkDoneForToday(taskId)
},
modifier = Modifier.animateItem(),
)

Expand Down Expand Up @@ -341,6 +358,10 @@ private fun SwipeableTaskItem(
onClick: (Long) -> Unit,
onUndoClick: (Long, TaskStatus) -> Unit,
onSwipe: (TaskStatus) -> Unit,
isContextMenuVisible: Boolean,
onOpenContextMenu: (Long) -> Unit,
onDismissContextMenu: () -> Unit,
onMarkDoneForToday: (Long) -> Unit,
modifier: Modifier = Modifier,
) {
val spacing = LocalTodozySpacing.current
Expand All @@ -355,6 +376,10 @@ private fun SwipeableTaskItem(
onSwipe = onSwipe,
onClick = onClick,
onUndoClick = onUndoClick,
isContextMenuVisible = isContextMenuVisible,
onOpenContextMenu = onOpenContextMenu,
onDismissContextMenu = onDismissContextMenu,
onMarkDoneForToday = onMarkDoneForToday,
enableSwipe = task.showInlineMetrics.not(),
)
}
Expand Down Expand Up @@ -401,6 +426,10 @@ private fun AnchoredSwipeToDismiss(
onSwipe: (TaskStatus) -> Unit,
onClick: (Long) -> Unit,
onUndoClick: (Long, TaskStatus) -> Unit,
isContextMenuVisible: Boolean,
onOpenContextMenu: (Long) -> Unit,
onDismissContextMenu: () -> Unit,
onMarkDoneForToday: (Long) -> Unit,
enableSwipe: Boolean,
) {
var widthPx by remember { mutableFloatStateOf(0f) }
Expand Down Expand Up @@ -511,6 +540,7 @@ private fun AnchoredSwipeToDismiss(
task = task,
onClick = onClick,
onUndoClick = onUndoClick,
onLongClick = { onOpenContextMenu(task.taskId) },
modifier =
Modifier
.fillMaxWidth()
Expand All @@ -522,5 +552,16 @@ private fun AnchoredSwipeToDismiss(
flingBehavior = flingBehavior,
),
)

DropdownMenu(
expanded = isContextMenuVisible,
onDismissRequest = onDismissContextMenu,
) {
DropdownMenuItem(
onClick = { onMarkDoneForToday(task.taskId) },
) {
Text(text = stringResource(id = R.string.task_menu_mark_done_for_today))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal sealed class TaskListViewIntent {
object OnClickMenuHistory : TaskListViewIntent()
object OnClickNewTask : TaskListViewIntent()
data class OnClickTask(val taskId: Long) : TaskListViewIntent()
data class OnMarkDoneForToday(val taskId: Long) : TaskListViewIntent()
data class OnSubmitSearchTerm(val term: String) : TaskListViewIntent()
data class OnSwipeTask(val taskId: Long, val status: TaskStatus) : TaskListViewIntent()
data class OnClickUndoTask(val taskId: Long, val status: TaskStatus) : TaskListViewIntent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import br.com.sailboat.todozy.feature.task.history.domain.model.TaskProgressFilt
import br.com.sailboat.todozy.feature.task.history.domain.usecase.GetTaskProgressUseCase
import br.com.sailboat.todozy.feature.task.list.domain.usecase.GetTasksUseCase
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.CompleteTaskUseCase
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.MarkTaskDoneForTodayUseCase
import br.com.sailboat.todozy.feature.task.list.impl.presentation.factory.TaskListUiModelFactory
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnClickMenuAbout
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnClickMenuHistory
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnClickMenuSettings
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnClickNewTask
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnClickTask
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnClickUndoTask
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnMarkDoneForToday
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnSelectProgressRange
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnStart
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent.OnSubmitSearchTerm
Expand Down Expand Up @@ -55,6 +57,7 @@ internal class TaskListViewModel(
private val getTaskMetricsUseCase: GetTaskMetricsUseCase,
private val getTaskProgressUseCase: GetTaskProgressUseCase,
private val completeTaskUseCase: CompleteTaskUseCase,
private val markTaskDoneForTodayUseCase: MarkTaskDoneForTodayUseCase,
private val taskListUiModelFactory: TaskListUiModelFactory,
private val logService: LogService,
private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider,
Expand Down Expand Up @@ -89,6 +92,7 @@ internal class TaskListViewModel(
is OnClickMenuHistory -> onClickMenuHistory()
is OnClickNewTask -> onClickNewTask()
is OnClickTask -> onClickTask(viewIntent.taskId)
is OnMarkDoneForToday -> onMarkDoneForToday(viewIntent.taskId)
is OnSubmitSearchTerm -> onSubmitSearchTerm(viewIntent.term)
is OnClickUndoTask -> onClickUndoTask(viewIntent.taskId)
is OnSwipeTask -> onSwipeTask(viewIntent.taskId, viewIntent.status)
Expand Down Expand Up @@ -158,6 +162,19 @@ internal class TaskListViewModel(
viewState.viewAction.tryEmit(TaskListViewAction.NavigateToTaskDetails(taskId = taskId))
}

private fun onMarkDoneForToday(taskId: Long) = viewModelScope.launch {
try {
withContext(dispatcherProvider.io()) {
markTaskDoneForTodayUseCase(taskId).getOrThrow()
}
updateTodayProgress(TaskStatus.DONE)
loadTaskMetrics()
} catch (e: Exception) {
logService.error(e)
viewState.viewAction.tryEmit(TaskListViewAction.ShowErrorCompletingTask)
}
}

private fun onSubmitSearchTerm(term: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
Expand Down
1 change: 1 addition & 0 deletions feature/task-list/impl/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fab_new_task">New Task</string>
<string name="task_menu_mark_done_for_today">Mark done for today</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package br.com.sailboat.todozy.feature.task.list.impl.domain.usecase

import br.com.sailboat.todozy.domain.model.Task
import br.com.sailboat.todozy.domain.model.TaskStatus
import br.com.sailboat.todozy.feature.task.details.domain.usecase.GetTaskUseCase
import br.com.sailboat.todozy.feature.task.history.domain.usecase.AddHistoryUseCase
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.confirmVerified
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Test
import kotlin.test.assertTrue

internal class MarkTaskDoneForTodayUseCaseImplTest {
private val getTaskUseCase: GetTaskUseCase = mockk(relaxed = true)
private val addHistoryUseCase: AddHistoryUseCase = mockk(relaxed = true)

private val markTaskDoneForTodayUseCase =
MarkTaskDoneForTodayUseCaseImpl(
getTaskUseCase = getTaskUseCase,
addHistoryUseCase = addHistoryUseCase,
)

@Test
fun `should add history for task id`() = runBlocking {
val task =
Task(
id = 42L,
name = "Task Name",
notes = "Some notes",
)
coEvery { getTaskUseCase(task.id) } returns Result.success(task)
coEvery { addHistoryUseCase(task, TaskStatus.DONE) } returns Result.success(Unit)

val result = markTaskDoneForTodayUseCase(task.id)

assertTrue(result.isSuccess)
coVerify { getTaskUseCase(task.id) }
coVerify { addHistoryUseCase(task, TaskStatus.DONE) }
confirmVerified(getTaskUseCase, addHistoryUseCase)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import br.com.sailboat.todozy.feature.task.history.domain.model.TaskProgressFilt
import br.com.sailboat.todozy.feature.task.history.domain.usecase.GetTaskProgressUseCase
import br.com.sailboat.todozy.feature.task.list.domain.usecase.GetTasksUseCase
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.CompleteTaskUseCase
import br.com.sailboat.todozy.feature.task.list.impl.domain.usecase.MarkTaskDoneForTodayUseCase
import br.com.sailboat.todozy.feature.task.list.impl.presentation.factory.TaskListUiModelFactory
import br.com.sailboat.todozy.utility.kotlin.LogService
import br.com.sailboat.todozy.utility.kotlin.model.Entity
Expand Down Expand Up @@ -52,6 +53,7 @@ internal class TaskListViewModelTest {
private val getTaskMetricsUseCase: GetTaskMetricsUseCase = mockk(relaxed = true)
private val getTaskProgressUseCase: GetTaskProgressUseCase = mockk(relaxed = true)
private val completeTaskUseCase: CompleteTaskUseCase = mockk(relaxed = true)
private val markTaskDoneForTodayUseCase: MarkTaskDoneForTodayUseCase = mockk(relaxed = true)
private val taskListUiModelFactory: TaskListUiModelFactory = mockk(relaxed = true)
private val logService: LogService = mockk(relaxed = true)

Expand All @@ -66,6 +68,7 @@ internal class TaskListViewModelTest {
getTaskMetricsUseCase = getTaskMetricsUseCase,
getTaskProgressUseCase = getTaskProgressUseCase,
completeTaskUseCase = completeTaskUseCase,
markTaskDoneForTodayUseCase = markTaskDoneForTodayUseCase,
taskListUiModelFactory = taskListUiModelFactory,
logService = logService,
dispatcherProvider = coroutinesTestRule.dispatcherProvider,
Expand Down Expand Up @@ -203,6 +206,17 @@ internal class TaskListViewModelTest {
assertEquals(TaskListViewAction.NavigateToHistory, latestAction())
}

@Test
fun `should call markTaskDoneForTodayUseCase when dispatchViewAction is called with OnMarkDoneForToday`() =
runTest(coroutinesTestRule.dispatcher) {
prepareScenario()

viewModel.dispatchViewIntent(TaskListViewIntent.OnMarkDoneForToday(taskId = 42L))
advanceUntilIdle()

coVerify { markTaskDoneForTodayUseCase(42L) }
}

@Test
fun `should navigate to settings screen when dispatchViewAction is called with OnClickMenuSettings`() {
prepareScenario()
Expand Down Expand Up @@ -585,6 +599,7 @@ internal class TaskListViewModelTest {
coEvery { getTaskProgressUseCase(any()) } returns Result.success(emptyList())
coEvery { getTaskMetricsUseCase(any()) } returns Result.success(TaskMetrics(0, 0, 0))
coEvery { completeTaskUseCase(any(), any()) } returns Result.success(Unit)
coEvery { markTaskDoneForTodayUseCase(any()) } returns Result.success(Unit)
}

private fun latestAction(): TaskListViewAction? = viewModel.viewState.viewAction.replayCache.lastOrNull()
Expand Down
Loading