mirror of
https://github.com/wismna/DoNext.git
synced 2026-07-25 12:13:04 -04:00
Fixes and technical improvements
This commit is contained in:
@@ -33,6 +33,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew :donextv2:testDebugUnitTest
|
||||
|
||||
- name: Decode signing key
|
||||
run: echo "${{ secrets.SIGNING_KEY_BASE64 }}" | base64 --decode > ${{ env.MODULE }}/upload.jks
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
/captures
|
||||
/fastlane/keys/
|
||||
/donext*/release/
|
||||
.claude
|
||||
@@ -20,8 +20,8 @@ android {
|
||||
applicationId = "com.wismna.geoffroy.donext"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 35
|
||||
versionName = "2.0.1"
|
||||
versionCode = 36
|
||||
versionName = "2.0.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -65,9 +65,6 @@ android {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.1.1"
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.wismna.geoffroy.donext.data.injection
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.repository.TaskListRepositoryImpl
|
||||
import com.wismna.geoffroy.donext.data.local.repository.TaskRepositoryImpl
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
@@ -15,4 +17,9 @@ abstract class RepositoryModule {
|
||||
abstract fun bindTaskRepository(
|
||||
impl: TaskRepositoryImpl
|
||||
): TaskRepository
|
||||
|
||||
@Binds
|
||||
abstract fun bindTaskListRepository(
|
||||
impl: TaskListRepositoryImpl
|
||||
): TaskListRepository
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.wismna.geoffroy.donext.data.local.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import com.wismna.geoffroy.donext.data.toDomain
|
||||
import com.wismna.geoffroy.donext.data.toEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class TaskListRepositoryImpl @Inject constructor(
|
||||
private val taskListDao: TaskListDao
|
||||
): TaskListRepository {
|
||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun getTaskListById(taskListId: Long): TaskList? {
|
||||
return taskListDao.getTaskListById(taskListId)?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun insertTaskList(taskList: TaskList) {
|
||||
taskListDao.insertTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun updateTaskList(taskList: TaskList) {
|
||||
taskListDao.updateTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) {
|
||||
taskListDao.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
|
||||
override fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>> {
|
||||
return taskListDao.getTaskListsWithOverdue(nowMillis)
|
||||
}
|
||||
}
|
||||
+2
-28
@@ -1,22 +1,17 @@
|
||||
package com.wismna.geoffroy.donext.data.local.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import com.wismna.geoffroy.donext.data.toDomain
|
||||
import com.wismna.geoffroy.donext.data.toEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskWithListName
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.map
|
||||
|
||||
class TaskRepositoryImpl @Inject constructor(
|
||||
private val taskDao: TaskDao,
|
||||
private val taskListDao: TaskListDao
|
||||
private val taskDao: TaskDao
|
||||
): TaskRepository {
|
||||
override fun getTasksForList(listId: Long): Flow<List<Task>> {
|
||||
return taskDao.getTasksForList(listId).map {entity -> entity.map { it.toDomain() }}
|
||||
@@ -58,28 +53,7 @@ class TaskRepositoryImpl @Inject constructor(
|
||||
taskDao.permanentDeleteAllDeletedTasks()
|
||||
}
|
||||
|
||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun getTaskListById(taskListId: Long): TaskList? {
|
||||
return taskListDao.getTaskListById(taskListId)?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun insertTaskList(taskList: TaskList) {
|
||||
taskListDao.insertTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun updateTaskList(taskList: TaskList) {
|
||||
taskListDao.updateTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) {
|
||||
override suspend fun toggleAllTasksInListDeleted(taskListId: Long, isDeleted: Boolean) {
|
||||
taskDao.toggleAllTasksFromListDeleted(taskListId, isDeleted)
|
||||
taskListDao.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
|
||||
override fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>> {
|
||||
return taskListDao.getTaskListsWithOverdue(nowMillis).map { it }
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.wismna.geoffroy.donext.domain.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TaskListRepository {
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun getTaskListById(taskListId: Long): TaskList?
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
suspend fun updateTaskList(taskList: TaskList)
|
||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
||||
fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>>
|
||||
}
|
||||
+1
-9
@@ -1,8 +1,6 @@
|
||||
package com.wismna.geoffroy.donext.domain.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskWithListName
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -17,11 +15,5 @@ interface TaskRepository {
|
||||
suspend fun toggleTaskDone(taskId: Long, isDone: Boolean)
|
||||
suspend fun permanentlyDeleteTask(taskId: Long)
|
||||
suspend fun permanentlyDeleteAllDeletedTask()
|
||||
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun getTaskListById(taskListId: Long): TaskList?
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
suspend fun updateTaskList(taskList: TaskList)
|
||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
||||
fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>>
|
||||
suspend fun toggleAllTasksInListDeleted(taskListId: Long, isDeleted: Boolean)
|
||||
}
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class AddTaskListUseCase @Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
private val repository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(title: String, order: Int) {
|
||||
repository.insertTaskList(
|
||||
|
||||
+6
-3
@@ -1,12 +1,15 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteTaskListUseCase@Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
class DeleteTaskListUseCase @Inject constructor(
|
||||
private val taskRepository: TaskRepository,
|
||||
private val taskListRepository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(taskListId: Long, isDeleted: Boolean) {
|
||||
repository.deleteTaskList(taskListId, isDeleted)
|
||||
taskRepository.toggleAllTasksInListDeleted(taskListId, isDeleted)
|
||||
taskListRepository.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsUseCase @Inject constructor(private val repository: TaskRepository) {
|
||||
class GetTaskListsUseCase @Inject constructor(private val repository: TaskListRepository) {
|
||||
operator fun invoke(): Flow<List<TaskList>> = repository.getTaskLists()
|
||||
}
|
||||
+3
-3
@@ -1,18 +1,18 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsWithOverdueUseCase @Inject constructor(
|
||||
private val taskRepository: TaskRepository,
|
||||
private val taskListRepository: TaskListRepository,
|
||||
private val clock: Clock
|
||||
) {
|
||||
operator fun invoke(): Flow<List<TaskListWithOverdue>> {
|
||||
return taskRepository.getTaskListsWithOverdue(
|
||||
return taskListRepository.getTaskListsWithOverdue(
|
||||
LocalDate.now(clock)
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
|
||||
+7
-5
@@ -1,23 +1,25 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class ToggleTaskDeletedUseCase @Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
private val taskRepository: TaskRepository,
|
||||
private val taskListRepository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(taskId: Long, isDeleted: Boolean) {
|
||||
if (!isDeleted) {
|
||||
val task = repository.getTaskById(taskId)
|
||||
val task = taskRepository.getTaskById(taskId)
|
||||
if (task != null) {
|
||||
// If task list was soft-deleted, restore it as well
|
||||
val taskList = repository.getTaskListById(task.taskListId)
|
||||
val taskList = taskListRepository.getTaskListById(task.taskListId)
|
||||
if (taskList != null && taskList.isDeleted) {
|
||||
repository.updateTaskList(taskList.copy(isDeleted = false))
|
||||
taskListRepository.updateTaskList(taskList.copy(isDeleted = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repository.toggleTaskDeleted(taskId, isDeleted)
|
||||
taskRepository.toggleTaskDeleted(taskId, isDeleted)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdateTaskListUseCase @Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
private val repository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(taskListId: Long, title: String, order: Int) {
|
||||
repository.updateTaskList(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||
package com.wismna.geoffroy.donext.presentation.model
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.extension.toLocalDate
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
@@ -10,7 +10,7 @@ import java.time.format.FormatStyle
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
class TaskItemViewModel(
|
||||
class TaskItemUiState(
|
||||
task: Task,
|
||||
private val clock: Clock = Clock.systemDefaultZone()) {
|
||||
val id: Long = task.id!!
|
||||
+2
-1
@@ -63,6 +63,7 @@ fun RecycleBinScreen(
|
||||
|
||||
val grouped = tasks.groupBy { it.listName }
|
||||
val context = LocalContext.current
|
||||
val taskDeletedMessage = stringResource(R.string.toast_task_permanently_deleted)
|
||||
|
||||
if (taskToDelete != null) {
|
||||
AlertDialog(
|
||||
@@ -75,7 +76,7 @@ fun RecycleBinScreen(
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.onConfirmDelete()
|
||||
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, taskDeletedMessage, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
|
||||
+18
-17
@@ -28,6 +28,7 @@ import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -41,7 +42,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.wismna.geoffroy.donext.R
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
|
||||
import com.wismna.geoffroy.donext.presentation.model.TaskItemUiState
|
||||
|
||||
@Composable
|
||||
fun TaskItemScreen(
|
||||
@@ -51,7 +52,7 @@ fun TaskItemScreen(
|
||||
onSwipeRight: () -> Unit,
|
||||
onTaskClick: (task: Task) -> Unit
|
||||
) {
|
||||
val viewModel = TaskItemViewModel(task)
|
||||
val uiState = remember(task) { TaskItemUiState(task) }
|
||||
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = {
|
||||
@@ -63,17 +64,17 @@ fun TaskItemScreen(
|
||||
)
|
||||
|
||||
val baseStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = when (viewModel.priority) {
|
||||
fontWeight = when (uiState.priority) {
|
||||
Priority.HIGH -> FontWeight.Bold
|
||||
Priority.NORMAL -> FontWeight.Normal
|
||||
Priority.LOW -> FontWeight.Normal
|
||||
},
|
||||
color = when (viewModel.priority) {
|
||||
color = when (uiState.priority) {
|
||||
Priority.HIGH -> MaterialTheme.colorScheme.onSurface
|
||||
Priority.NORMAL -> MaterialTheme.colorScheme.onSurface
|
||||
Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||
textDecoration = if (uiState.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||
)
|
||||
Card(
|
||||
modifier = modifier,
|
||||
@@ -87,8 +88,8 @@ fun TaskItemScreen(
|
||||
backgroundContent = {
|
||||
DismissBackground(
|
||||
dismissState,
|
||||
viewModel.isDone,
|
||||
viewModel.isDeleted
|
||||
uiState.isDone,
|
||||
uiState.isDeleted
|
||||
)
|
||||
},
|
||||
content = {
|
||||
@@ -97,7 +98,7 @@ fun TaskItemScreen(
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(8.dp)
|
||||
.alpha(if (viewModel.isDone || viewModel.priority == Priority.LOW) 0.5f else 1f),
|
||||
.alpha(if (uiState.isDone || uiState.priority == Priority.LOW) 0.5f else 1f),
|
||||
verticalAlignment = Alignment.CenterVertically // centers checkbox + content
|
||||
) {
|
||||
Box(
|
||||
@@ -108,12 +109,12 @@ fun TaskItemScreen(
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = viewModel.name,
|
||||
text = uiState.name,
|
||||
fontSize = 18.sp,
|
||||
style = baseStyle,
|
||||
modifier = Modifier
|
||||
.align(
|
||||
if (viewModel.description.isNullOrBlank()) Alignment.CenterStart
|
||||
if (uiState.description.isNullOrBlank()) Alignment.CenterStart
|
||||
else Alignment.TopStart
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -121,19 +122,19 @@ fun TaskItemScreen(
|
||||
)
|
||||
|
||||
// Due date badge
|
||||
viewModel.dueDateText?.let { dueMillis ->
|
||||
uiState.dueDateText?.let { dueMillis ->
|
||||
Badge(
|
||||
modifier = Modifier
|
||||
.align(
|
||||
if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd
|
||||
if (uiState.description.isNullOrBlank()) Alignment.CenterEnd
|
||||
else Alignment.TopEnd
|
||||
),
|
||||
containerColor = if (viewModel.isOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer
|
||||
containerColor = if (uiState.isOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 1.dp, end = 1.dp),
|
||||
text = viewModel.dueDateText,
|
||||
color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
text = uiState.dueDateText,
|
||||
color = if (uiState.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@@ -147,9 +148,9 @@ fun TaskItemScreen(
|
||||
.padding(top = 24.dp),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
if (!viewModel.description.isNullOrBlank()) {
|
||||
if (!uiState.description.isNullOrBlank()) {
|
||||
Text(
|
||||
text = viewModel.description,
|
||||
text = uiState.description,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = baseStyle.copy(
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
|
||||
@@ -189,7 +189,7 @@ fun TaskScreen(
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) { Text("Cancel") }
|
||||
) { Text(stringResource(R.string.task_cancel)) }
|
||||
|
||||
// --- Save Button ---
|
||||
Button(
|
||||
|
||||
+5
-3
@@ -71,12 +71,14 @@ class TaskViewModel @Inject constructor(
|
||||
|
||||
fun save(onDone: (() -> Unit)? = null) {
|
||||
if (title.isBlank()) return
|
||||
val listId = taskListId ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
if (isEditing()) {
|
||||
updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority, dueDate, isDone)
|
||||
val currentEditingTaskId = editingTaskId
|
||||
if (currentEditingTaskId != null) {
|
||||
updateTaskUseCase(currentEditingTaskId, listId, title, description, priority, dueDate, isDone)
|
||||
} else {
|
||||
createTaskUseCase(taskListId!!, title, description, priority, dueDate)
|
||||
createTaskUseCase(listId, title, description, priority, dueDate)
|
||||
}
|
||||
onDone?.invoke()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<string name="dialog_delete_task_description">Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action ne peut être annulée.</string>
|
||||
<string name="dialog_delete_task_cancel">Annuler</string>
|
||||
<string name="dialog_delete_task_delete">Supprimer</string>
|
||||
<string name="toast_task_permanently_deleted">Tâche supprimée</string>
|
||||
<string name="dialog_empty_task_title">Vider la corbeille</string>
|
||||
<string name="dialog_empty_task_description">Êtes-vous sûr de vider la corbeille ? Cette action ne peut être annulée.</string>
|
||||
<string name="dialog_empty_task_cancel">Annuler</string>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<string name="dialog_delete_task_description">Are you sure you want to permanently delete this task? This cannot be undone.</string>
|
||||
<string name="dialog_delete_task_cancel">Cancel</string>
|
||||
<string name="dialog_delete_task_delete">Delete</string>
|
||||
<string name="toast_task_permanently_deleted">Task deleted</string>
|
||||
|
||||
<string name="dialog_empty_task_title">Empty Recycle Bin</string>
|
||||
<string name="dialog_empty_task_description">Are you sure you want to permanently delete all tasks in the recycle bin? This cannot be undone.</string>
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DeleteTaskListUseCaseTest {
|
||||
|
||||
private val taskRepository = mockk<TaskRepository>(relaxed = true)
|
||||
private val taskListRepository = mockk<TaskListRepository>(relaxed = true)
|
||||
private val useCase = DeleteTaskListUseCase(taskRepository, taskListRepository)
|
||||
|
||||
@Test
|
||||
fun `deleting a list also toggles all its tasks, list last`() = runTest {
|
||||
useCase(5L, true)
|
||||
|
||||
coVerifyOrder {
|
||||
taskRepository.toggleAllTasksInListDeleted(5L, true)
|
||||
taskListRepository.deleteTaskList(5L, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restoring a list also restores all its tasks`() = runTest {
|
||||
useCase(5L, false)
|
||||
|
||||
coVerify { taskRepository.toggleAllTasksInListDeleted(5L, false) }
|
||||
coVerify { taskListRepository.deleteTaskList(5L, false) }
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -2,7 +2,7 @@ package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
@@ -25,7 +25,7 @@ class GetTaskListsWithOverdueUseCaseTest {
|
||||
val today = LocalDate.of(2026, 7, 16)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
val repository = mockk<TaskListRepository>()
|
||||
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(emptyList())
|
||||
|
||||
GetTaskListsWithOverdueUseCase(repository, clock).invoke()
|
||||
@@ -40,7 +40,7 @@ class GetTaskListsWithOverdueUseCaseTest {
|
||||
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
|
||||
val lists = listOf(TaskListWithOverdue(id = 1L, name = "Work", overdueCount = 2))
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
val repository = mockk<TaskListRepository>()
|
||||
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(lists)
|
||||
|
||||
val result = GetTaskListsWithOverdueUseCase(repository, clock).invoke().first()
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class ToggleTaskDeletedUseCaseTest {
|
||||
|
||||
private val taskRepository = mockk<TaskRepository>(relaxed = true)
|
||||
private val taskListRepository = mockk<TaskListRepository>(relaxed = true)
|
||||
private val useCase = ToggleTaskDeletedUseCase(taskRepository, taskListRepository)
|
||||
|
||||
private val task = Task(
|
||||
id = 1L,
|
||||
taskListId = 5L,
|
||||
name = "Task",
|
||||
description = "",
|
||||
priority = Priority.NORMAL,
|
||||
isDone = false,
|
||||
isDeleted = true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `restoring a task also restores its soft-deleted parent list`() = runTest {
|
||||
val deletedList = TaskList(id = 5L, name = "List", isDeleted = true, order = 0)
|
||||
coEvery { taskRepository.getTaskById(1L) } returns task
|
||||
coEvery { taskListRepository.getTaskListById(5L) } returns deletedList
|
||||
|
||||
useCase(1L, false)
|
||||
|
||||
coVerify { taskListRepository.updateTaskList(deletedList.copy(isDeleted = false)) }
|
||||
coVerify { taskRepository.toggleTaskDeleted(1L, false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restoring a task whose parent list is not deleted leaves the list untouched`() = runTest {
|
||||
val activeList = TaskList(id = 5L, name = "List", isDeleted = false, order = 0)
|
||||
coEvery { taskRepository.getTaskById(1L) } returns task
|
||||
coEvery { taskListRepository.getTaskListById(5L) } returns activeList
|
||||
|
||||
useCase(1L, false)
|
||||
|
||||
coVerify(exactly = 0) { taskListRepository.updateTaskList(any()) }
|
||||
coVerify { taskRepository.toggleTaskDeleted(1L, false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `soft-deleting a task never touches its parent list`() = runTest {
|
||||
useCase(1L, true)
|
||||
|
||||
coVerify(exactly = 0) { taskRepository.getTaskById(any()) }
|
||||
coVerify(exactly = 0) { taskListRepository.getTaskListById(any()) }
|
||||
coVerify { taskRepository.toggleTaskDeleted(1L, true) }
|
||||
}
|
||||
}
|
||||
+25
-25
@@ -1,4 +1,4 @@
|
||||
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||
package com.wismna.geoffroy.donext.presentation.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
@@ -11,7 +11,7 @@ import java.time.format.FormatStyle
|
||||
import java.time.format.TextStyle
|
||||
import java.util.*
|
||||
|
||||
class TaskItemViewModelTest {
|
||||
class TaskItemUiStateTest {
|
||||
|
||||
private val fixedClock: Clock = Clock.fixed(
|
||||
LocalDate.of(2025, 1, 10)
|
||||
@@ -47,80 +47,80 @@ class TaskItemViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `initializes fields from Task`() {
|
||||
val viewModel = TaskItemViewModel(baseTask)
|
||||
val uiState = TaskItemUiState(baseTask)
|
||||
|
||||
assertThat(viewModel.id).isEqualTo(baseTask.id)
|
||||
assertThat(viewModel.name).isEqualTo(baseTask.name)
|
||||
assertThat(viewModel.description).isEqualTo(baseTask.description)
|
||||
assertThat(viewModel.isDone).isFalse()
|
||||
assertThat(viewModel.isDeleted).isFalse()
|
||||
assertThat(viewModel.priority).isEqualTo(Priority.NORMAL)
|
||||
assertThat(uiState.id).isEqualTo(baseTask.id)
|
||||
assertThat(uiState.name).isEqualTo(baseTask.name)
|
||||
assertThat(uiState.description).isEqualTo(baseTask.description)
|
||||
assertThat(uiState.isDone).isFalse()
|
||||
assertThat(uiState.isDeleted).isFalse()
|
||||
assertThat(uiState.priority).isEqualTo(Priority.NORMAL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverdue is true when due date is before today`() {
|
||||
val overdueTask = baseTask.copy(dueDate = millisForDaysFromFixedToday(-1))
|
||||
val viewModel = TaskItemViewModel(overdueTask)
|
||||
val uiState = TaskItemUiState(overdueTask)
|
||||
|
||||
assertThat(viewModel.isOverdue).isTrue()
|
||||
assertThat(uiState.isOverdue).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverdue is false when due date is today`() {
|
||||
val dueToday = baseTask.copy(dueDate = millisForDaysFromFixedToday(0))
|
||||
val viewModel = TaskItemViewModel(dueToday, fixedClock)
|
||||
val uiState = TaskItemUiState(dueToday, fixedClock)
|
||||
|
||||
assertThat(viewModel.isOverdue).isFalse()
|
||||
assertThat(uiState.isOverdue).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverdue is false when due date is null`() {
|
||||
val viewModel = TaskItemViewModel(baseTask.copy(dueDate = null))
|
||||
val uiState = TaskItemUiState(baseTask.copy(dueDate = null))
|
||||
|
||||
assertThat(viewModel.isOverdue).isFalse()
|
||||
assertThat(uiState.isOverdue).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is Today when due date is today`() {
|
||||
val dueToday = baseTask.copy(dueDate = millisForDaysFromFixedToday(0))
|
||||
val viewModel = TaskItemViewModel(dueToday, fixedClock)
|
||||
val uiState = TaskItemUiState(dueToday, fixedClock)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo("Today")
|
||||
assertThat(uiState.dueDateText).isEqualTo("Today")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is Tomorrow when due date is tomorrow`() {
|
||||
val dueTomorrow = baseTask.copy(dueDate = millisForDaysFromFixedToday(1))
|
||||
val viewModel = TaskItemViewModel(dueTomorrow, fixedClock)
|
||||
val uiState = TaskItemUiState(dueTomorrow, fixedClock)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo("Tomorrow")
|
||||
assertThat(uiState.dueDateText).isEqualTo("Tomorrow")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is Yesterday when due date was yesterday`() {
|
||||
val dueYesterday = baseTask.copy(dueDate = millisForDaysFromFixedToday(-1))
|
||||
val viewModel = TaskItemViewModel(dueYesterday, fixedClock)
|
||||
val uiState = TaskItemUiState(dueYesterday, fixedClock)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo("Yesterday")
|
||||
assertThat(uiState.dueDateText).isEqualTo("Yesterday")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is day of week when within next 7 days`() {
|
||||
val dueIn3Days = baseTask.copy(dueDate = millisForDaysFromFixedToday(3))
|
||||
val viewModel = TaskItemViewModel(dueIn3Days, fixedClock)
|
||||
val uiState = TaskItemUiState(dueIn3Days, fixedClock)
|
||||
|
||||
val expected = today
|
||||
.plusDays(3)
|
||||
.dayOfWeek
|
||||
.getDisplayName(TextStyle.SHORT, Locale.getDefault())
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo(expected)
|
||||
assertThat(uiState.dueDateText).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is formatted date when more than 7 days away`() {
|
||||
val dueIn10Days = baseTask.copy(dueDate = millisForDaysFromFixedToday(10))
|
||||
val viewModel = TaskItemViewModel(dueIn10Days)
|
||||
val uiState = TaskItemUiState(dueIn10Days)
|
||||
|
||||
val expected = today
|
||||
.plusDays(10)
|
||||
@@ -129,6 +129,6 @@ class TaskItemViewModelTest {
|
||||
.withLocale(Locale.getDefault())
|
||||
)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo(expected)
|
||||
assertThat(uiState.dueDateText).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
+10
@@ -180,6 +180,16 @@ class TaskViewModelTest {
|
||||
coVerify(exactly = 0) { updateTaskUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save before taskListId is set does nothing`() = runTest {
|
||||
viewModel.onTitleChanged("Task")
|
||||
viewModel.save()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { createTaskUseCase(any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { updateTaskUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save creates task when not editing`() = runTest {stickyEventsFlow.emit(UiEvent.CreateNewTask(3L))
|
||||
advanceUntilIdle()
|
||||
|
||||
Reference in New Issue
Block a user