From 6c889fa48f6a131db67e43a5a3d81549ee9ba423 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 21 Apr 2024 11:53:07 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Alarm=20Selection=20in=20the?= =?UTF-8?q?=20Add=20Task=20Bottom=20Sheet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a quality of life feature to quickly add alarms while the task is being created. This creates a better UX and allow users to use Alkaa as a quick reminder without going to an additional screen. --- .../kotlin/com/escodro/task/di/TaskModule.kt | 1 + .../task/presentation/add/AddTaskViewModel.kt | 19 +++++- .../task/presentation/add/TaskBottomSheet.kt | 32 ++++++++-- .../presentation/add/AddTaskViewModelTest.kt | 58 +++++++++++++++++-- .../task/presentation/fake/AddTaskFake.kt | 12 ++-- 5 files changed, 103 insertions(+), 19 deletions(-) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt b/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt index 51c3b15a4..8bf0c7a33 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt @@ -50,6 +50,7 @@ val taskModule = module { viewModelDefinition { AddTaskViewModel( addTaskUseCase = get(), + alarmIntervalMapper = get(), applicationScope = get(), ) } diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/AddTaskViewModel.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/AddTaskViewModel.kt index 427a2fbd7..ac7b74c27 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/AddTaskViewModel.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/AddTaskViewModel.kt @@ -3,19 +3,34 @@ package com.escodro.task.presentation.add import com.escodro.coroutines.AppCoroutineScope import com.escodro.domain.model.Task import com.escodro.domain.usecase.task.AddTask +import com.escodro.task.mapper.AlarmIntervalMapper +import com.escodro.task.model.AlarmInterval import com.escodro.task.presentation.detail.main.CategoryId import dev.icerock.moko.mvvm.viewmodel.ViewModel +import kotlinx.datetime.LocalDateTime internal class AddTaskViewModel( private val addTaskUseCase: AddTask, + private val alarmIntervalMapper: AlarmIntervalMapper, private val applicationScope: AppCoroutineScope, ) : ViewModel() { - fun addTask(title: String, categoryId: CategoryId?) { + fun addTask( + title: String, + categoryId: CategoryId?, + dueDate: LocalDateTime?, + alarmInterval: AlarmInterval = AlarmInterval.NEVER, + ) { if (title.isBlank()) return + val interval = alarmIntervalMapper.toDomain(alarmInterval) applicationScope.launch { - val task = Task(title = title, categoryId = categoryId?.value) + val task = Task( + title = title, + dueDate = dueDate, + categoryId = categoryId?.value, + alarmInterval = interval, + ) addTaskUseCase.invoke(task) } } diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/TaskBottomSheet.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/TaskBottomSheet.kt index 9e51f3097..c21598a1d 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/TaskBottomSheet.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/add/TaskBottomSheet.kt @@ -1,13 +1,11 @@ package com.escodro.task.presentation.add -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -21,31 +19,37 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp +import com.escodro.alarmapi.AlarmPermission import com.escodro.categoryapi.presentation.CategoryListViewModel import com.escodro.categoryapi.presentation.CategoryState import com.escodro.designsystem.components.AlkaaInputTextField import com.escodro.resources.MR +import com.escodro.task.model.AlarmInterval import com.escodro.task.presentation.category.CategorySelection +import com.escodro.task.presentation.detail.alarm.AlarmSelection import com.escodro.task.presentation.detail.main.CategoryId import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay +import kotlinx.datetime.LocalDateTime import org.koin.compose.koinInject @Composable internal fun AddTaskBottomSheet( addTaskViewModel: AddTaskViewModel = koinInject(), categoryViewModel: CategoryListViewModel = koinInject(), + alarmPermission: AlarmPermission = koinInject(), onHideBottomSheet: () -> Unit, ) { Column( modifier = Modifier .fillMaxWidth() - .height(256.dp) - .background(MaterialTheme.colorScheme.surface) // Accompanist does not support M3 yet + .fillMaxWidth(0.5f) .padding(16.dp), verticalArrangement = Arrangement.SpaceAround, ) { - var taskInputText by rememberSaveable { mutableStateOf("") } + var taskInputText: String by rememberSaveable { mutableStateOf("") } + var taskDueDate: LocalDateTime? by rememberSaveable { mutableStateOf(null) } + var alarmInterval: AlarmInterval by rememberSaveable { mutableStateOf(AlarmInterval.NEVER) } val categoryState by remember(categoryViewModel) { categoryViewModel }.loadCategories().collectAsState(initial = CategoryState.Empty) @@ -72,12 +76,28 @@ internal fun AddTaskBottomSheet( onCategoryChange = { categoryId -> currentCategory = categoryId }, ) + AlarmSelection( + calendar = taskDueDate, + interval = alarmInterval, + onAlarmUpdate = { dateTime -> taskDueDate = dateTime }, + onIntervalSelect = { interval -> alarmInterval = interval }, + hasExactAlarmPermission = { alarmPermission.hasExactAlarmPermission() }, + openExactAlarmPermissionScreen = { alarmPermission.openExactAlarmPermissionScreen() }, + openAppSettingsScreen = { alarmPermission.openAppSettings() }, + ) + Button( modifier = Modifier + .padding(top = 8.dp, bottom = 16.dp) .fillMaxWidth() .height(48.dp), onClick = { - addTaskViewModel.addTask(taskInputText, currentCategory) + addTaskViewModel.addTask( + title = taskInputText, + categoryId = currentCategory, + dueDate = taskDueDate, + alarmInterval = alarmInterval, + ) taskInputText = "" onHideBottomSheet() }, diff --git a/features/task/src/test/java/com/escodro/task/presentation/add/AddTaskViewModelTest.kt b/features/task/src/test/java/com/escodro/task/presentation/add/AddTaskViewModelTest.kt index d1f4f9543..7a21ae8aa 100644 --- a/features/task/src/test/java/com/escodro/task/presentation/add/AddTaskViewModelTest.kt +++ b/features/task/src/test/java/com/escodro/task/presentation/add/AddTaskViewModelTest.kt @@ -1,10 +1,13 @@ package com.escodro.task.presentation.add import com.escodro.coroutines.AppCoroutineScope +import com.escodro.task.mapper.AlarmIntervalMapper +import com.escodro.task.model.AlarmInterval import com.escodro.task.presentation.detail.main.CategoryId import com.escodro.task.presentation.fake.AddTaskFake import com.escodro.test.rule.CoroutinesTestDispatcher import com.escodro.test.rule.CoroutinesTestDispatcherImpl +import kotlinx.datetime.LocalDateTime import org.junit.Assert import org.junit.Before import org.junit.Test @@ -13,8 +16,11 @@ internal class AddTaskViewModelTest : CoroutinesTestDispatcher by CoroutinesTest private val addTask = AddTaskFake() + private val alarmIntervalMapper = AlarmIntervalMapper() + private val viewModel = AddTaskViewModel( addTaskUseCase = addTask, + alarmIntervalMapper = alarmIntervalMapper, applicationScope = AppCoroutineScope(context = testDispatcher()), ) @@ -27,17 +33,61 @@ internal class AddTaskViewModelTest : CoroutinesTestDispatcher by CoroutinesTest fun `test if when a task is created when function is called`() { val taskTitle = "Rendez-vous" - viewModel.addTask(taskTitle, CategoryId(null)) + viewModel.addTask( + title = taskTitle, + categoryId = CategoryId(null), + dueDate = null, + alarmInterval = AlarmInterval.NEVER, + ) - Assert.assertTrue(addTask.wasTaskCreated(taskTitle)) + Assert.assertEquals(taskTitle, addTask.createdTask?.title) } @Test fun `test if when a task is not created when title is empty`() { val taskTitle = "" - viewModel.addTask(taskTitle, CategoryId(null)) + viewModel.addTask( + title = taskTitle, + categoryId = CategoryId(value = null), + dueDate = null, + alarmInterval = AlarmInterval.MONTHLY, + ) + + Assert.assertTrue(addTask.createdTask == null) + } + + @Test + fun `test if due date is created in the task`() { + val taskTitle = "Voulez-vous?" + val dueDate = LocalDateTime(2022, 1, 1, 12, 0) + + viewModel.addTask( + title = taskTitle, + categoryId = CategoryId(null), + dueDate = dueDate, + alarmInterval = AlarmInterval.NEVER, + ) + + Assert.assertEquals(taskTitle, addTask.createdTask?.title) + Assert.assertEquals(dueDate, addTask.createdTask?.dueDate) + } + + @Test + fun `test if alarm interval is created in the task`() { + val taskTitle = "Coucher avec moi?" + val alarmInterval = AlarmInterval.WEEKLY + + viewModel.addTask( + title = taskTitle, + categoryId = CategoryId(null), + dueDate = null, + alarmInterval = alarmInterval, + ) + + val assertAlarmInterval = alarmIntervalMapper.toDomain(alarmInterval) - Assert.assertFalse(addTask.wasTaskCreated(taskTitle)) + Assert.assertEquals(taskTitle, addTask.createdTask?.title) + Assert.assertEquals(assertAlarmInterval, addTask.createdTask?.alarmInterval) } } diff --git a/features/task/src/test/java/com/escodro/task/presentation/fake/AddTaskFake.kt b/features/task/src/test/java/com/escodro/task/presentation/fake/AddTaskFake.kt index 8392b0cd7..7ea2ff8fa 100644 --- a/features/task/src/test/java/com/escodro/task/presentation/fake/AddTaskFake.kt +++ b/features/task/src/test/java/com/escodro/task/presentation/fake/AddTaskFake.kt @@ -5,15 +5,13 @@ import com.escodro.domain.usecase.task.AddTask internal class AddTaskFake : AddTask { - private val updatedList: MutableList = mutableListOf() + var createdTask: Task? = null override suspend fun invoke(task: Task) { - updatedList.add(task) + createdTask = task } - fun clear() = - updatedList.clear() - - fun wasTaskCreated(title: String): Boolean = - updatedList.any { it.title == title } + fun clear() { + createdTask = null + } }