From 5888790ba70ad3ba5536cb462dd25b558ff27919 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 19 Oct 2026 10:18:47 +0000
Subject: [PATCH 01/14] Create task screen priority field
Add priority selector UI component and integrate with CreateTask screen
and ViewModel to allow selecting and persisting task priority.
Closes #123
---
.../composeResources/values/strings.xml | 1 +
.../prioritypicker/FoliaryPriorityPicker.kt | 131 ++++++++++++++++++
.../feature/createtask/CreateTaskScreen.kt | 11 ++
.../feature/createtask/CreateTaskViewModel.kt | 10 +-
.../FoliaryPriorityPickerTest.kt | 103 ++++++++++++++
.../createtask/CreateTaskScreenTest.kt | 28 ++++
.../createtask/CreateTaskViewDataFixture.kt | 4 +
.../createtask/CreateTaskViewModelTest.kt | 91 ++++++++++++
8 files changed, 378 insertions(+), 1 deletion(-)
create mode 100644 foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt
create mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt
diff --git a/foliary/src/commonMain/composeResources/values/strings.xml b/foliary/src/commonMain/composeResources/values/strings.xml
index 2480cf4..3bbfc0b 100644
--- a/foliary/src/commonMain/composeResources/values/strings.xml
+++ b/foliary/src/commonMain/composeResources/values/strings.xml
@@ -71,6 +71,7 @@
Add description...
Save
Title is required
+ Priority
Due date
Add due date
Clear due date
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt
new file mode 100644
index 0000000..6b5b9d6
--- /dev/null
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt
@@ -0,0 +1,131 @@
+package dev.appoutlet.foliary.core.ui.component.prioritypicker
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.dp
+import com.composables.icons.lucide.Check
+import com.composables.icons.lucide.ChevronDown
+import com.composables.icons.lucide.Flag
+import com.composables.icons.lucide.Lucide
+import dev.appoutlet.foliary.core.ui.component.card.FoliaryCard
+import dev.appoutlet.foliary.core.ui.component.pill.PriorityPill
+import dev.appoutlet.foliary.data.task.database.entity.Priority
+import foliary.foliary.generated.resources.Res
+import foliary.foliary.generated.resources.priority_pill_blocker
+import foliary.foliary.generated.resources.priority_pill_high
+import foliary.foliary.generated.resources.priority_pill_highest
+import foliary.foliary.generated.resources.priority_pill_low
+import foliary.foliary.generated.resources.priority_pill_lowest
+import foliary.foliary.generated.resources.priority_pill_medium
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun FoliaryPriorityPicker(
+ selectedPriority: Priority,
+ onPrioritySelected: (Priority) -> Unit,
+ modifier: Modifier = Modifier,
+ label: String,
+) {
+ var showMenu by rememberSaveable { mutableStateOf(false) }
+
+ Box(modifier = modifier) {
+ PriorityPickerCard(
+ label = label,
+ selectedPriority = selectedPriority,
+ onClick = { showMenu = true }
+ )
+
+ DropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false },
+ containerColor = MaterialTheme.colorScheme.surface,
+ ) {
+ Priority.entries.forEach { priority ->
+ DropdownMenuItem(
+ text = { Text(text = stringResource(priority.stringResource)) },
+ leadingIcon = { Icon(imageVector = Lucide.Flag, contentDescription = null) },
+ trailingIcon = if (priority == selectedPriority) {
+ { Icon(imageVector = Lucide.Check, contentDescription = null) }
+ } else {
+ null
+ },
+ onClick = {
+ showMenu = false
+ onPrioritySelected(priority)
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun PriorityPickerCard(
+ label: String,
+ selectedPriority: Priority,
+ onClick: () -> Unit,
+) {
+ FoliaryCard(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("FoliaryPriorityPicker:Card")
+ .clickable(onClick = onClick),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .weight(1f),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+
+ PriorityPill(
+ priority = selectedPriority,
+ modifier = Modifier.testTag("FoliaryPriorityPicker:Value")
+ )
+ }
+
+ Icon(
+ modifier = Modifier.padding(end = 16.dp),
+ imageVector = Lucide.ChevronDown,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+}
+
+private val Priority.stringResource: StringResource
+ get() = when (this) {
+ Priority.LOWEST -> Res.string.priority_pill_lowest
+ Priority.LOW -> Res.string.priority_pill_low
+ Priority.MEDIUM -> Res.string.priority_pill_medium
+ Priority.HIGH -> Res.string.priority_pill_high
+ Priority.HIGHEST -> Res.string.priority_pill_highest
+ Priority.BLOCKER -> Res.string.priority_pill_blocker
+ }
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
index 74a6ba2..4af14ce 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
@@ -24,6 +24,7 @@ import dev.appoutlet.foliary.core.ui.component.button.FoliaryBackIconButton
import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton
import dev.appoutlet.foliary.core.ui.component.datepicker.FoliaryDatePicker
import dev.appoutlet.foliary.core.ui.component.modifier.widthInCompact
+import dev.appoutlet.foliary.core.ui.component.prioritypicker.FoliaryPriorityPicker
import dev.appoutlet.foliary.core.ui.component.textfield.FoliaryTextField
import dev.appoutlet.foliary.feature.main.getWindowDecorationPadding
import foliary.foliary.generated.resources.Res
@@ -31,6 +32,7 @@ import foliary.foliary.generated.resources.create_task_description_label
import foliary.foliary.generated.resources.create_task_description_placeholder
import foliary.foliary.generated.resources.create_task_due_date_label
import foliary.foliary.generated.resources.create_task_due_date_placeholder
+import foliary.foliary.generated.resources.create_task_priority_label
import foliary.foliary.generated.resources.create_task_save
import foliary.foliary.generated.resources.create_task_title
import foliary.foliary.generated.resources.create_task_title_label
@@ -57,6 +59,15 @@ fun CreateTaskScreen(viewData: CreateTaskViewData, onEvent: (CreateTaskEvent) ->
Spacer(Modifier.size(16.dp))
TitleField(viewData.title, onEvent)
DescriptionField(viewData.description, onEvent)
+ FoliaryPriorityPicker(
+ modifier = Modifier.widthInCompact()
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .testTag("CreateTaskScreen:PriorityPicker"),
+ label = stringResource(Res.string.create_task_priority_label),
+ selectedPriority = viewData.priority,
+ onPrioritySelected = { onEvent(CreateTaskEvent.PriorityChanged(it)) },
+ )
FoliaryDatePicker(
modifier = Modifier.widthInCompact()
.fillMaxWidth()
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt
index 335ecb7..f6836f6 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt
@@ -38,6 +38,7 @@ class CreateTaskViewModel(
id = task.id.toString(),
title = task.title,
description = task.description,
+ priority = task.priority,
dueDate = task.dueDate?.let { dueDate ->
CreateTaskViewData.DueDateViewData(
selectedDateMillis = dueDate.toEpochMilliseconds(),
@@ -54,12 +55,17 @@ class CreateTaskViewModel(
when (event) {
is CreateTaskEvent.TitleChanged -> onTitleChange(event.title)
is CreateTaskEvent.DescriptionChanged -> onDescriptionChange(event.description)
+ is CreateTaskEvent.PriorityChanged -> onPriorityChange(event.priority)
is CreateTaskEvent.DueDateChanged -> onDueDateChange(event.dueDateMillis)
CreateTaskEvent.SaveClicked -> onSaveClick()
CreateTaskEvent.BackClicked -> onBackClick()
}
}
+ private fun onPriorityChange(priority: Priority) = intent {
+ reduce { state.copy(priority = priority) }
+ }
+
private fun onDueDateChange(dueDateMillis: Long?) = intent {
reduce {
state.copy(
@@ -100,7 +106,7 @@ class CreateTaskViewModel(
timeProvider.endOfDay(it)
},
completionDate = null,
- priority = Priority.MEDIUM,
+ priority = state.priority,
url = null,
location = null,
)
@@ -121,6 +127,7 @@ data class CreateTaskViewData(
val id: String? = null,
val title: String = "",
val description: String? = null,
+ val priority: Priority = Priority.MEDIUM,
val dueDate: DueDateViewData? = null,
val minDueDateMillis: Long,
val saveButtonEnabled: Boolean = false,
@@ -138,6 +145,7 @@ data class CreateTaskViewData(
sealed interface CreateTaskEvent {
data class TitleChanged(val title: String) : CreateTaskEvent
data class DescriptionChanged(val description: String?) : CreateTaskEvent
+ data class PriorityChanged(val priority: Priority) : CreateTaskEvent
data class DueDateChanged(val dueDateMillis: Long?) : CreateTaskEvent
data object SaveClicked : CreateTaskEvent
data object BackClicked : CreateTaskEvent
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt
new file mode 100644
index 0000000..e91dc1b
--- /dev/null
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt
@@ -0,0 +1,103 @@
+package dev.appoutlet.foliary.core.ui.component.prioritypicker
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.v2.runComposeUiTest
+import dev.appoutlet.foliary.data.task.database.entity.Priority
+import foliary.foliary.generated.resources.Res
+import foliary.foliary.generated.resources.priority_pill_blocker
+import foliary.foliary.generated.resources.priority_pill_high
+import foliary.foliary.generated.resources.priority_pill_highest
+import foliary.foliary.generated.resources.priority_pill_low
+import foliary.foliary.generated.resources.priority_pill_lowest
+import foliary.foliary.generated.resources.priority_pill_medium
+import io.kotest.matchers.shouldBe
+import org.jetbrains.compose.resources.getString
+import kotlin.test.Test
+
+@OptIn(ExperimentalTestApi::class)
+class FoliaryPriorityPickerTest {
+
+ @Test
+ fun `should render selected priority as pill`() = runComposeUiTest {
+ val label = "Priority"
+ val mediumText = getString(Res.string.priority_pill_medium)
+
+ setContent {
+ FoliaryPriorityPicker(
+ selectedPriority = Priority.MEDIUM,
+ onPrioritySelected = {},
+ label = label
+ )
+ }
+
+ onNodeWithText(label).assertIsDisplayed()
+ onNodeWithText(mediumText).assertIsDisplayed()
+ onNodeWithTag("FoliaryPriorityPicker:Value", useUnmergedTree = true).assertIsDisplayed()
+ }
+
+ @Test
+ fun `no menu items should exist before card is clicked`() = runComposeUiTest {
+ setContent {
+ FoliaryPriorityPicker(
+ selectedPriority = Priority.MEDIUM,
+ onPrioritySelected = {},
+ label = "Priority"
+ )
+ }
+
+ onNodeWithText(getString(Res.string.priority_pill_lowest)).assertDoesNotExist()
+ onNodeWithText(getString(Res.string.priority_pill_high)).assertDoesNotExist()
+ onNodeWithText(getString(Res.string.priority_pill_blocker)).assertDoesNotExist()
+ }
+
+ @Test
+ fun `should open menu and list all six labels when card is clicked`() = runComposeUiTest {
+ setContent {
+ FoliaryPriorityPicker(
+ selectedPriority = Priority.MEDIUM,
+ onPrioritySelected = {},
+ label = "Priority"
+ )
+ }
+
+ onNodeWithTag("FoliaryPriorityPicker:Card", useUnmergedTree = true).performClick()
+ waitForIdle()
+
+ onNodeWithText(getString(Res.string.priority_pill_lowest)).assertIsDisplayed()
+ onNodeWithText(getString(Res.string.priority_pill_low)).assertIsDisplayed()
+ onAllNodesWithText(getString(Res.string.priority_pill_medium)).assertCountEquals(2)
+ onNodeWithText(getString(Res.string.priority_pill_high)).assertIsDisplayed()
+ onNodeWithText(getString(Res.string.priority_pill_highest)).assertIsDisplayed()
+ onNodeWithText(getString(Res.string.priority_pill_blocker)).assertIsDisplayed()
+ }
+
+ @Test
+ fun `selecting an option should invoke callback and close menu`() = runComposeUiTest {
+ var selectedPriority: Priority? = null
+
+ setContent {
+ FoliaryPriorityPicker(
+ selectedPriority = Priority.MEDIUM,
+ onPrioritySelected = { selectedPriority = it },
+ label = "Priority"
+ )
+ }
+
+ onNodeWithTag("FoliaryPriorityPicker:Card", useUnmergedTree = true).performClick()
+ waitForIdle()
+
+ onNodeWithText(getString(Res.string.priority_pill_high)).performClick()
+ waitForIdle()
+
+ selectedPriority shouldBe Priority.HIGH
+
+ onNodeWithText(getString(Res.string.priority_pill_lowest)).assertDoesNotExist()
+ onNodeWithText(getString(Res.string.priority_pill_blocker)).assertDoesNotExist()
+ }
+}
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt
index 2a73f93..c0996d7 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runComposeUiTest
+import dev.appoutlet.foliary.data.task.database.entity.Priority
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.back_icon_button_a11y
import foliary.foliary.generated.resources.create_task_description_label
@@ -21,10 +22,13 @@ import foliary.foliary.generated.resources.create_task_description_placeholder
import foliary.foliary.generated.resources.create_task_due_date_clear_a11y
import foliary.foliary.generated.resources.create_task_due_date_label
import foliary.foliary.generated.resources.create_task_due_date_placeholder
+import foliary.foliary.generated.resources.create_task_priority_label
import foliary.foliary.generated.resources.create_task_save
import foliary.foliary.generated.resources.create_task_title
import foliary.foliary.generated.resources.create_task_title_label
import foliary.foliary.generated.resources.create_task_title_placeholder
+import foliary.foliary.generated.resources.priority_pill_high
+import foliary.foliary.generated.resources.priority_pill_medium
import io.kotest.matchers.shouldBe
import org.jetbrains.compose.resources.getString
import kotlin.test.Test
@@ -50,6 +54,8 @@ class CreateTaskScreenTest {
onNodeWithText(getString(Res.string.create_task_title_placeholder)).assertIsDisplayed()
onNodeWithText(getString(Res.string.create_task_description_label)).assertIsDisplayed()
onNodeWithText(getString(Res.string.create_task_description_placeholder)).assertIsDisplayed()
+ onNodeWithTag("CreateTaskScreen:PriorityPicker").assertIsDisplayed()
+ onNodeWithText(getString(Res.string.priority_pill_medium)).assertIsDisplayed()
onNodeWithText(getString(Res.string.create_task_due_date_label)).assertIsDisplayed()
onNodeWithText(getString(Res.string.create_task_due_date_placeholder)).assertIsDisplayed()
onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y))
@@ -149,4 +155,26 @@ class CreateTaskScreenTest {
onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y))
.assertDoesNotExist()
}
+
+ @Test
+ fun `should change priority and emit event`() = runComposeUiTest {
+ val events = mutableListOf()
+ var viewData by mutableStateOf(CreateTaskViewData.fixture(priority = Priority.MEDIUM))
+
+ setContent {
+ CreateTaskScreen(viewData = viewData, onEvent = events::add)
+ }
+
+ onNodeWithTag("CreateTaskScreen:PriorityPicker").performClick()
+ waitForIdle()
+
+ onNodeWithText(getString(Res.string.priority_pill_high)).performClick()
+
+ events.contains(CreateTaskEvent.PriorityChanged(Priority.HIGH)) shouldBe true
+
+ viewData = viewData.copy(priority = Priority.HIGH)
+ waitForIdle()
+
+ onNodeWithText(getString(Res.string.priority_pill_high)).assertIsDisplayed()
+ }
}
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt
index 0a9b3da..7a767f2 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt
@@ -1,9 +1,12 @@
package dev.appoutlet.foliary.feature.createtask
+import dev.appoutlet.foliary.data.task.database.entity.Priority
+
fun CreateTaskViewData.Companion.fixture(
id: String? = null,
title: String = "Task fixture",
description: String? = "Task description",
+ priority: Priority = Priority.MEDIUM,
dueDate: CreateTaskViewData.DueDateViewData? = CreateTaskViewData.DueDateViewData.fixture(),
minDueDateMillis: Long = 1_752_960_000_000,
saveButtonEnabled: Boolean = true,
@@ -11,6 +14,7 @@ fun CreateTaskViewData.Companion.fixture(
id = id,
title = title,
description = description,
+ priority = priority,
dueDate = dueDate,
minDueDateMillis = minDueDateMillis,
saveButtonEnabled = saveButtonEnabled,
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
index d140f15..106539c 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
@@ -4,6 +4,7 @@ import dev.appoutlet.foliary.core.provider.time.TimeProvider
import dev.appoutlet.foliary.core.provider.uuid.UuidProvider
import dev.appoutlet.foliary.core.testing.ViewModelTest
import dev.appoutlet.foliary.data.task.TaskRepository
+import dev.appoutlet.foliary.data.task.database.entity.Priority
import dev.appoutlet.foliary.data.task.database.entity.Task
import dev.appoutlet.foliary.data.task.database.entity.fixture
import dev.mokkery.MockMode
@@ -75,6 +76,17 @@ class CreateTaskViewModelTest :
expectSideEffect(CreateTaskAction.NavigateBack)
}
+ @Test
+ fun `default state should have medium priority`() = test {
+ currentState.priority shouldBe Priority.MEDIUM
+ }
+
+ @Test
+ fun `PriorityChanged - should update priority`() = test {
+ viewModel.onEvent(CreateTaskEvent.PriorityChanged(Priority.HIGH))
+ expectState { copy(priority = Priority.HIGH) }
+ }
+
@Test
fun `default state should have dueDate unset and minDueDate set to start of today`() = test {
currentState.dueDate shouldBe null
@@ -185,6 +197,85 @@ class CreateTaskViewModelTest :
awaitSideEffect() shouldBe CreateTaskAction.NavigateBack
}
+ @Test
+ fun `SaveClicked - should persist default medium priority`() = test {
+ val id = Uuid.random()
+ val title = "Task title"
+ val taskCapture = Capture.slot()
+
+ every(mockUuidProvider::random) returns id
+ every(mockTimeProvider::now) returns Instant.parse("2026-07-21T12:00:00Z")
+ everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
+
+ viewModel.onEvent(CreateTaskEvent.TitleChanged(title))
+ expectState { copy(title = title, saveButtonEnabled = true) }
+
+ viewModel.onEvent(CreateTaskEvent.SaveClicked)
+ awaitState().saveButtonEnabled shouldBe false
+
+ taskCapture.get().priority shouldBe Priority.MEDIUM
+ }
+
+ @Test
+ fun `SaveClicked - should persist selected priority`() = test {
+ val id = Uuid.random()
+ val title = "Task title"
+ val taskCapture = Capture.slot()
+
+ every(mockUuidProvider::random) returns id
+ every(mockTimeProvider::now) returns Instant.parse("2026-07-21T12:00:00Z")
+ everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
+
+ viewModel.onEvent(CreateTaskEvent.TitleChanged(title))
+ expectState { copy(title = title, saveButtonEnabled = true) }
+
+ viewModel.onEvent(CreateTaskEvent.PriorityChanged(Priority.BLOCKER))
+ expectState { copy(title = title, priority = Priority.BLOCKER, saveButtonEnabled = true) }
+
+ viewModel.onEvent(CreateTaskEvent.SaveClicked)
+ awaitState().saveButtonEnabled shouldBe false
+
+ taskCapture.get().priority shouldBe Priority.BLOCKER
+ }
+
+ @Test
+ fun `should preserve saved task priority on init`() {
+ val taskId = Uuid.random()
+ val savedTask = Task.fixture(id = taskId, priority = Priority.HIGHEST, dueDate = null)
+ val taskCapture = Capture.slot()
+
+ everySuspend { mockTaskRepository.findById(taskId) } returns savedTask
+ every(mockTimeProvider::now) returns Instant.parse("2026-07-22T12:00:00Z")
+ everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
+
+ viewModel = CreateTaskViewModel(
+ taskId = taskId.toString(),
+ taskRepository = mockTaskRepository,
+ timeProvider = mockTimeProvider,
+ uuidProvider = mockUuidProvider,
+ )
+
+ test {
+ expectState {
+ copy(
+ id = taskId.toString(),
+ title = savedTask.title,
+ description = savedTask.description,
+ priority = Priority.HIGHEST,
+ dueDate = null,
+ saveButtonEnabled = true,
+ )
+ }
+
+ viewModel.onEvent(CreateTaskEvent.SaveClicked)
+ awaitState().saveButtonEnabled shouldBe false
+
+ taskCapture.get().priority shouldBe Priority.HIGHEST
+
+ awaitSideEffect() shouldBe CreateTaskAction.NavigateBack
+ }
+ }
+
@Test
fun `SaveClicked - should keep creationDate when editing an existing task`() {
val taskId = Uuid.random()
From 1017a4f7598960c4321089b9a3ba9e971354d5cd Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 19 Oct 2026 10:52:13 +0000
Subject: [PATCH 02/14] Create task screen priority field
Add priority selector UI component and integrate with CreateTask screen
and ViewModel to allow selecting and persisting task priority.
Closes #123
---
.../appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt | 1 -
1 file changed, 1 deletion(-)
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt
index c0996d7..00b4927 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt
@@ -22,7 +22,6 @@ import foliary.foliary.generated.resources.create_task_description_placeholder
import foliary.foliary.generated.resources.create_task_due_date_clear_a11y
import foliary.foliary.generated.resources.create_task_due_date_label
import foliary.foliary.generated.resources.create_task_due_date_placeholder
-import foliary.foliary.generated.resources.create_task_priority_label
import foliary.foliary.generated.resources.create_task_save
import foliary.foliary.generated.resources.create_task_title
import foliary.foliary.generated.resources.create_task_title_label
From 7dfdcf213ab4a6e7fd5993385e85340d7dfa92a9 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 20 Oct 2026 08:20:41 +0000
Subject: [PATCH 03/14] Create task screen priority field
Add priority selector UI component in feature package and integrate with CreateTask
screen and ViewModel to allow selecting and persisting task priority.
Closes #123
---
.../appoutlet/foliary/feature/createtask/CreateTaskScreen.kt | 1 -
.../createtask}/FoliaryPriorityPicker.kt | 2 +-
.../createtask}/FoliaryPriorityPickerTest.kt | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
rename foliary/src/commonMain/kotlin/dev/appoutlet/foliary/{core/ui/component/prioritypicker => feature/createtask}/FoliaryPriorityPicker.kt (98%)
rename foliary/src/commonTest/kotlin/dev/appoutlet/foliary/{core/ui/component/prioritypicker => feature/createtask}/FoliaryPriorityPickerTest.kt (98%)
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
index 4af14ce..14d4ff1 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt
@@ -24,7 +24,6 @@ import dev.appoutlet.foliary.core.ui.component.button.FoliaryBackIconButton
import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton
import dev.appoutlet.foliary.core.ui.component.datepicker.FoliaryDatePicker
import dev.appoutlet.foliary.core.ui.component.modifier.widthInCompact
-import dev.appoutlet.foliary.core.ui.component.prioritypicker.FoliaryPriorityPicker
import dev.appoutlet.foliary.core.ui.component.textfield.FoliaryTextField
import dev.appoutlet.foliary.feature.main.getWindowDecorationPadding
import foliary.foliary.generated.resources.Res
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
similarity index 98%
rename from foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt
rename to foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
index 6b5b9d6..e909b80 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPicker.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
@@ -1,4 +1,4 @@
-package dev.appoutlet.foliary.core.ui.component.prioritypicker
+package dev.appoutlet.foliary.feature.createtask
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt
similarity index 98%
rename from foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt
rename to foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt
index e91dc1b..56c2417 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/prioritypicker/FoliaryPriorityPickerTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt
@@ -1,4 +1,4 @@
-package dev.appoutlet.foliary.core.ui.component.prioritypicker
+package dev.appoutlet.foliary.feature.createtask
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertCountEquals
From baac73189bc8d6e32b2b8a72d7c5d14da7d8f9c6 Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:02:45 +0100
Subject: [PATCH 04/14] priority ui
---
.../core/ui/component/pill/PriorityPill.kt | 33 +------
.../core/ui/priority/PriorityViewData.kt | 96 +++++++++++++++++++
.../data/task/database/entity/Priority.kt | 2 +-
.../createtask/FoliaryPriorityPicker.kt | 65 ++++++-------
gradle/gradle-daemon-jvm.properties | 23 ++---
5 files changed, 147 insertions(+), 72 deletions(-)
create mode 100644 foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt
index 09bb04c..c5605d9 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt
@@ -2,48 +2,25 @@ package dev.appoutlet.foliary.core.ui.component.pill
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.composables.icons.lucide.Flag
-import com.composables.icons.lucide.Lucide
+import dev.appoutlet.foliary.core.ui.priority.viewData
import dev.appoutlet.foliary.core.ui.theme.FoliaryTheme
import dev.appoutlet.foliary.data.task.database.entity.Priority
-import foliary.foliary.generated.resources.Res
-import foliary.foliary.generated.resources.priority_pill_blocker
-import foliary.foliary.generated.resources.priority_pill_high
-import foliary.foliary.generated.resources.priority_pill_highest
-import foliary.foliary.generated.resources.priority_pill_low
-import foliary.foliary.generated.resources.priority_pill_lowest
-import foliary.foliary.generated.resources.priority_pill_medium
import org.jetbrains.compose.resources.stringResource
-private val HighContentColor = Color(0xFFC4A319)
-private val HighestContentColor = Color(0xFFAD5203)
-
@Composable
fun PriorityPill(priority: Priority, modifier: Modifier = Modifier) {
- val defaultContentColor = MaterialTheme.colorScheme.onSurface
- val blockerContentColor = MaterialTheme.colorScheme.error
-
- val (text, contentColor) = when (priority) {
- Priority.LOWEST -> Res.string.priority_pill_lowest to defaultContentColor
- Priority.LOW -> Res.string.priority_pill_low to defaultContentColor
- Priority.MEDIUM -> Res.string.priority_pill_medium to defaultContentColor
- Priority.HIGH -> Res.string.priority_pill_high to HighContentColor
- Priority.HIGHEST -> Res.string.priority_pill_highest to HighestContentColor
- Priority.BLOCKER -> Res.string.priority_pill_blocker to blockerContentColor
- }
+ val priorityViewData = priority.viewData()
FoliaryPill(
modifier = modifier.testTag("PriorityPill"),
- icon = Lucide.Flag,
- text = stringResource(text),
- contentColor = contentColor
+ icon = priorityViewData.icon,
+ text = priorityViewData.text,
+ contentColor = priorityViewData.color
)
}
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
new file mode 100644
index 0000000..f1a337c
--- /dev/null
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
@@ -0,0 +1,96 @@
+package dev.appoutlet.foliary.core.ui.priority
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.composables.icons.lucide.ChevronDown
+import com.composables.icons.lucide.ChevronUp
+import com.composables.icons.lucide.ChevronsDown
+import com.composables.icons.lucide.ChevronsUp
+import com.composables.icons.lucide.ChevronsUpDown
+import com.composables.icons.lucide.CircleMinus
+import com.composables.icons.lucide.Lucide
+import dev.appoutlet.foliary.data.task.database.entity.Priority
+import foliary.foliary.generated.resources.Res
+import foliary.foliary.generated.resources.priority_pill_blocker
+import foliary.foliary.generated.resources.priority_pill_high
+import foliary.foliary.generated.resources.priority_pill_highest
+import foliary.foliary.generated.resources.priority_pill_low
+import foliary.foliary.generated.resources.priority_pill_lowest
+import foliary.foliary.generated.resources.priority_pill_medium
+import org.jetbrains.compose.resources.stringResource
+
+data class FoliaryPriorityViewData(
+ val text: String,
+ val color: Color,
+ val icon: ImageVector,
+)
+
+val PriorityHighLight = Color(0xFFBD8100)
+val PriorityHighestLight = Color(0xFFA65200)
+val PriorityBlockerLight = Color(0xFF93000A)
+
+val PriorityHighDark = Color(0xFFFFAA5C)
+val PriorityHighestDark = Color(0xFFFF8080)
+val PriorityBlockerDark = Color(0xFFFF5449)
+
+private data class PriorityColors(
+ val high: Color,
+ val highest: Color,
+ val blocker: Color
+)
+
+@Composable
+fun Priority.viewData(): FoliaryPriorityViewData {
+ val backgroundColor = MaterialTheme.colorScheme.background
+
+ val colors = remember(this) {
+ val isDarkTheme = backgroundColor.luminance() < .5f
+ if (isDarkTheme) {
+ PriorityColors(PriorityHighDark, PriorityHighestDark, PriorityBlockerDark)
+ } else {
+ PriorityColors(PriorityHighLight, PriorityHighestLight, PriorityBlockerLight)
+ }
+ }
+
+ return when (this) {
+ Priority.LOWEST -> FoliaryPriorityViewData(
+ text = stringResource(Res.string.priority_pill_lowest),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = .4f),
+ icon = Lucide.ChevronsDown,
+ )
+
+ Priority.LOW -> FoliaryPriorityViewData(
+ text = stringResource(Res.string.priority_pill_low),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = .7f),
+ icon = Lucide.ChevronDown,
+ )
+
+ Priority.MEDIUM -> FoliaryPriorityViewData(
+ text = stringResource(Res.string.priority_pill_medium),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 1f),
+ icon = Lucide.ChevronsUpDown,
+ )
+
+ Priority.HIGH -> FoliaryPriorityViewData(
+ text = stringResource(Res.string.priority_pill_high),
+ color = colors.high,
+ icon = Lucide.ChevronUp,
+ )
+
+ Priority.HIGHEST -> FoliaryPriorityViewData(
+ text = stringResource(Res.string.priority_pill_highest),
+ color = colors.highest,
+ icon = Lucide.ChevronsUp,
+ )
+
+ Priority.BLOCKER -> FoliaryPriorityViewData(
+ text = stringResource(Res.string.priority_pill_blocker),
+ color = colors.blocker,
+ icon = Lucide.CircleMinus,
+ )
+ }
+}
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt
index a8f8735..80e54c8 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/entity/Priority.kt
@@ -1,5 +1,5 @@
package dev.appoutlet.foliary.data.task.database.entity
enum class Priority {
- LOWEST, LOW, MEDIUM, HIGH, HIGHEST, BLOCKER
+ BLOCKER, HIGHEST, HIGH, MEDIUM, LOW, LOWEST
}
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
index e909b80..670ff42 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
@@ -11,6 +11,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -23,20 +24,10 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.Check
import com.composables.icons.lucide.ChevronDown
-import com.composables.icons.lucide.Flag
import com.composables.icons.lucide.Lucide
import dev.appoutlet.foliary.core.ui.component.card.FoliaryCard
-import dev.appoutlet.foliary.core.ui.component.pill.PriorityPill
+import dev.appoutlet.foliary.core.ui.priority.viewData
import dev.appoutlet.foliary.data.task.database.entity.Priority
-import foliary.foliary.generated.resources.Res
-import foliary.foliary.generated.resources.priority_pill_blocker
-import foliary.foliary.generated.resources.priority_pill_high
-import foliary.foliary.generated.resources.priority_pill_highest
-import foliary.foliary.generated.resources.priority_pill_low
-import foliary.foliary.generated.resources.priority_pill_lowest
-import foliary.foliary.generated.resources.priority_pill_medium
-import org.jetbrains.compose.resources.StringResource
-import org.jetbrains.compose.resources.stringResource
@Composable
fun FoliaryPriorityPicker(
@@ -60,18 +51,23 @@ fun FoliaryPriorityPicker(
containerColor = MaterialTheme.colorScheme.surface,
) {
Priority.entries.forEach { priority ->
+ val viewData = priority.viewData()
DropdownMenuItem(
- text = { Text(text = stringResource(priority.stringResource)) },
- leadingIcon = { Icon(imageVector = Lucide.Flag, contentDescription = null) },
- trailingIcon = if (priority == selectedPriority) {
- { Icon(imageVector = Lucide.Check, contentDescription = null) }
- } else {
- null
+ text = { Text(text = viewData.text) },
+ leadingIcon = { Icon(imageVector = viewData.icon, contentDescription = null) },
+ trailingIcon = {
+ if (priority == selectedPriority) {
+ Icon(imageVector = Lucide.Check, contentDescription = null)
+ }
},
onClick = {
showMenu = false
onPrioritySelected(priority)
- }
+ },
+ colors = MenuDefaults.itemColors(
+ textColor = viewData.color,
+ leadingIconColor = viewData.color
+ )
)
}
}
@@ -84,6 +80,7 @@ private fun PriorityPickerCard(
selectedPriority: Priority,
onClick: () -> Unit,
) {
+ val viewData = selectedPriority.viewData()
FoliaryCard(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
@@ -104,10 +101,24 @@ private fun PriorityPickerCard(
color = MaterialTheme.colorScheme.onBackground
)
- PriorityPill(
- priority = selectedPriority,
- modifier = Modifier.testTag("FoliaryPriorityPicker:Value")
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+
+ Icon(
+ imageVector = viewData.icon,
+ contentDescription = null,
+ tint = viewData.color
+ )
+
+ Text(
+ modifier = Modifier.testTag("FoliaryPriorityPicker:Value"),
+ text = viewData.text,
+ color = viewData.color,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
}
Icon(
@@ -119,13 +130,3 @@ private fun PriorityPickerCard(
}
}
}
-
-private val Priority.stringResource: StringResource
- get() = when (this) {
- Priority.LOWEST -> Res.string.priority_pill_lowest
- Priority.LOW -> Res.string.priority_pill_low
- Priority.MEDIUM -> Res.string.priority_pill_medium
- Priority.HIGH -> Res.string.priority_pill_high
- Priority.HIGHEST -> Res.string.priority_pill_highest
- Priority.BLOCKER -> Res.string.priority_pill_blocker
- }
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
index 6c1139e..c610302 100644
--- a/gradle/gradle-daemon-jvm.properties
+++ b/gradle/gradle-daemon-jvm.properties
@@ -1,12 +1,13 @@
#This file is generated by updateDaemonJvm
-toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
-toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
-toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
-toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
-toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
-toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
-toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
-toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
-toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
-toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
-toolchainVersion=21
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cba046b7dcb2d003be9d45bd846e7569/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/28d2e80ff6ae2ef68c116d735e66a048/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/f947c0a30aa9016da9bb0912d406d3e8/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c5047562b7a2d3671802428a0793774/redirect
+toolchainVendor=JETBRAINS
+toolchainVersion=25
From 2d6c4ea0219931cdcf90824e67d76dd1a371c493 Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:03:53 +0100
Subject: [PATCH 05/14] jvm 21
---
gradle/gradle-daemon-jvm.properties | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
index c610302..baa28d1 100644
--- a/gradle/gradle-daemon-jvm.properties
+++ b/gradle/gradle-daemon-jvm.properties
@@ -1,13 +1,13 @@
#This file is generated by updateDaemonJvm
-toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect
-toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect
-toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect
-toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect
-toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cba046b7dcb2d003be9d45bd846e7569/redirect
-toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/28d2e80ff6ae2ef68c116d735e66a048/redirect
-toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d57918ebcf16bdd07b10454e594d88e4/redirect
-toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/104aa12b204c38a5934ad22c8b1f1eb6/redirect
-toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/f947c0a30aa9016da9bb0912d406d3e8/redirect
-toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c5047562b7a2d3671802428a0793774/redirect
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
toolchainVendor=JETBRAINS
-toolchainVersion=25
+toolchainVersion=21
From d4eb64ee178500a06c28df52ce113fd0b3b1b2de Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:04:46 +0100
Subject: [PATCH 06/14] refactor: rename FoliaryPriorityViewData to
PriorityViewData
---
.../core/ui/component/pill/PriorityPill.kt | 1 -
.../foliary/core/ui/priority/PriorityViewData.kt | 16 ++++++++--------
.../feature/createtask/FoliaryPriorityPicker.kt | 1 -
3 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt
index c5605d9..6045c00 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/pill/PriorityPill.kt
@@ -10,7 +10,6 @@ import androidx.compose.ui.unit.dp
import dev.appoutlet.foliary.core.ui.priority.viewData
import dev.appoutlet.foliary.core.ui.theme.FoliaryTheme
import dev.appoutlet.foliary.data.task.database.entity.Priority
-import org.jetbrains.compose.resources.stringResource
@Composable
fun PriorityPill(priority: Priority, modifier: Modifier = Modifier) {
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
index f1a337c..44600f7 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
@@ -23,7 +23,7 @@ import foliary.foliary.generated.resources.priority_pill_lowest
import foliary.foliary.generated.resources.priority_pill_medium
import org.jetbrains.compose.resources.stringResource
-data class FoliaryPriorityViewData(
+data class PriorityViewData(
val text: String,
val color: Color,
val icon: ImageVector,
@@ -44,7 +44,7 @@ private data class PriorityColors(
)
@Composable
-fun Priority.viewData(): FoliaryPriorityViewData {
+fun Priority.viewData(): PriorityViewData {
val backgroundColor = MaterialTheme.colorScheme.background
val colors = remember(this) {
@@ -57,37 +57,37 @@ fun Priority.viewData(): FoliaryPriorityViewData {
}
return when (this) {
- Priority.LOWEST -> FoliaryPriorityViewData(
+ Priority.LOWEST -> PriorityViewData(
text = stringResource(Res.string.priority_pill_lowest),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = .4f),
icon = Lucide.ChevronsDown,
)
- Priority.LOW -> FoliaryPriorityViewData(
+ Priority.LOW -> PriorityViewData(
text = stringResource(Res.string.priority_pill_low),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = .7f),
icon = Lucide.ChevronDown,
)
- Priority.MEDIUM -> FoliaryPriorityViewData(
+ Priority.MEDIUM -> PriorityViewData(
text = stringResource(Res.string.priority_pill_medium),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 1f),
icon = Lucide.ChevronsUpDown,
)
- Priority.HIGH -> FoliaryPriorityViewData(
+ Priority.HIGH -> PriorityViewData(
text = stringResource(Res.string.priority_pill_high),
color = colors.high,
icon = Lucide.ChevronUp,
)
- Priority.HIGHEST -> FoliaryPriorityViewData(
+ Priority.HIGHEST -> PriorityViewData(
text = stringResource(Res.string.priority_pill_highest),
color = colors.highest,
icon = Lucide.ChevronsUp,
)
- Priority.BLOCKER -> FoliaryPriorityViewData(
+ Priority.BLOCKER -> PriorityViewData(
text = stringResource(Res.string.priority_pill_blocker),
color = colors.blocker,
icon = Lucide.CircleMinus,
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
index 670ff42..4d9fbe8 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
@@ -105,7 +105,6 @@ private fun PriorityPickerCard(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
-
Icon(
imageVector = viewData.icon,
contentDescription = null,
From d945666f4d08d14274653e978c822459110814d1 Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:10:10 +0100
Subject: [PATCH 07/14] refactor(ui): introduce LocalIsDarkTheme composition
local
- Added `LocalIsDarkTheme` `CompositionLocal` and provided it inside `Theme.kt`.
- Updated `Priority.viewData()` to determine the theme mode using `LocalIsDarkTheme` instead of calculating background color luminance.
---
.../foliary/core/ui/priority/PriorityViewData.kt | 7 +++----
.../dev/appoutlet/foliary/core/ui/theme/Color.kt | 3 +++
.../dev/appoutlet/foliary/core/ui/theme/Theme.kt | 15 +++++++++------
3 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
index 44600f7..fe931b5 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/priority/PriorityViewData.kt
@@ -4,7 +4,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.vector.ImageVector
import com.composables.icons.lucide.ChevronDown
import com.composables.icons.lucide.ChevronUp
@@ -13,6 +12,7 @@ import com.composables.icons.lucide.ChevronsUp
import com.composables.icons.lucide.ChevronsUpDown
import com.composables.icons.lucide.CircleMinus
import com.composables.icons.lucide.Lucide
+import dev.appoutlet.foliary.core.ui.theme.LocalIsDarkTheme
import dev.appoutlet.foliary.data.task.database.entity.Priority
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.priority_pill_blocker
@@ -45,10 +45,9 @@ private data class PriorityColors(
@Composable
fun Priority.viewData(): PriorityViewData {
- val backgroundColor = MaterialTheme.colorScheme.background
+ val isDarkTheme = LocalIsDarkTheme.current
- val colors = remember(this) {
- val isDarkTheme = backgroundColor.luminance() < .5f
+ val colors = remember(isDarkTheme) {
if (isDarkTheme) {
PriorityColors(PriorityHighDark, PriorityHighestDark, PriorityBlockerDark)
} else {
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt
index 3aafdec..243438d 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Color.kt
@@ -1,4 +1,5 @@
package dev.appoutlet.foliary.core.ui.theme
+import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF1A4D3E)
@@ -72,3 +73,5 @@ val surfaceContainerLowDark = Color(0xFF22261E)
val surfaceContainerDark = Color(0xFF272B22)
val surfaceContainerHighDark = Color(0xFF32372B)
val surfaceContainerHighestDark = Color(0xFF3D4235)
+
+val LocalIsDarkTheme = compositionLocalOf { false }
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt
index 3b5de6d..c8073e2 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/theme/Theme.kt
@@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
private val lightScheme = lightColorScheme(
primary = primaryLight,
@@ -91,12 +92,14 @@ fun FoliaryTheme(
val colorScheme = if (isDarkTheme) darkScheme else lightScheme
- MaterialTheme(
- colorScheme = colorScheme,
- typography = getTypography(colorScheme),
- content = content,
- shapes = shapes
- )
+ CompositionLocalProvider(LocalIsDarkTheme provides isDarkTheme) {
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = getTypography(colorScheme),
+ content = content,
+ shapes = shapes
+ )
+ }
}
@Composable
From 94b79d44e50b8706a790f9cab117c3d5871b93ad Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:15:17 +0100
Subject: [PATCH 08/14] ```text refactor: reorder parameters in
FoliaryPriorityPicker ```
---
.../foliary/feature/createtask/FoliaryPriorityPicker.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
index 4d9fbe8..6dd605c 100644
--- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
+++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPicker.kt
@@ -31,10 +31,10 @@ import dev.appoutlet.foliary.data.task.database.entity.Priority
@Composable
fun FoliaryPriorityPicker(
+ label: String,
selectedPriority: Priority,
onPrioritySelected: (Priority) -> Unit,
modifier: Modifier = Modifier,
- label: String,
) {
var showMenu by rememberSaveable { mutableStateOf(false) }
From 13f0da42c374905dd57d928540e9d5eb36b9cdfa Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:27:41 +0100
Subject: [PATCH 09/14] ci: run allTests for test coverage check in PR workflow
---
.github/workflows/pull-request.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index a411a4d..b120bbe 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -36,7 +36,7 @@ jobs:
run: ./gradlew detekt
- name: Check code test coverage
- run: ./gradlew jvmTest -Pkover koverVerify
+ run: ./gradlew allTests -Pkover koverVerify
- name: Upload JVM test report
if: always()
From 98666dd92919e877c522d2e10218d5ab542190bd Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:30:58 +0100
Subject: [PATCH 10/14] CreateTaskViewModelTest
---
.../createtask/CreateTaskViewModelTest.kt | 116 +++++++-----------
1 file changed, 41 insertions(+), 75 deletions(-)
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
index 106539c..ebb3f56 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt
@@ -17,7 +17,6 @@ import dev.mokkery.matcher.capture.capture
import dev.mokkery.matcher.capture.get
import dev.mokkery.mock
import io.kotest.matchers.shouldBe
-import io.kotest.matchers.shouldNotBe
import kotlin.test.Test
import kotlin.time.Instant
import kotlin.uuid.Uuid
@@ -42,14 +41,11 @@ class CreateTaskViewModelTest :
}
@Test
- fun `TitleChanged - blank title button disabled`() = test {
+ fun `TitleChanged - should update title and save button enabled state`() = test {
+ val title = "Task"
+
viewModel.onEvent(CreateTaskEvent.TitleChanged(""))
currentState.saveButtonEnabled shouldBe false
- }
-
- @Test
- fun `TitleChanged - entering a valid title enables the save button`() = test {
- val title = "Task"
viewModel.onEvent(CreateTaskEvent.TitleChanged(title))
@@ -77,8 +73,16 @@ class CreateTaskViewModelTest :
}
@Test
- fun `default state should have medium priority`() = test {
- currentState.priority shouldBe Priority.MEDIUM
+ fun `default state should have expected values`() = test {
+ currentState shouldBe CreateTaskViewData(
+ id = null,
+ title = "",
+ description = null,
+ priority = Priority.MEDIUM,
+ dueDate = null,
+ minDueDateMillis = startOfToday.toEpochMilliseconds(),
+ saveButtonEnabled = false,
+ )
}
@Test
@@ -87,18 +91,16 @@ class CreateTaskViewModelTest :
expectState { copy(priority = Priority.HIGH) }
}
- @Test
- fun `default state should have dueDate unset and minDueDate set to start of today`() = test {
- currentState.dueDate shouldBe null
- currentState.minDueDateMillis shouldBe startOfToday.toEpochMilliseconds()
- }
-
@Test
fun `should load saved task on init`() {
val taskId = Uuid.random()
- val savedTask = Task.fixture(id = taskId)
+ val savedTask = Task.fixture(id = taskId, priority = Priority.HIGHEST)
+ val taskCapture = Capture.slot()
+ val endOfDayDueDate = Instant.parse("2026-08-21T23:59:59.999999999Z")
everySuspend { mockTaskRepository.findById(taskId) } returns savedTask
+ every { mockTimeProvider.endOfDay(any()) } returns endOfDayDueDate
+ everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
viewModel = CreateTaskViewModel(
taskId = taskId.toString(),
@@ -113,6 +115,7 @@ class CreateTaskViewModelTest :
id = taskId.toString(),
title = savedTask.title,
description = savedTask.description,
+ priority = savedTask.priority,
dueDate = savedTask.dueDate?.let { dueDate ->
CreateTaskViewData.DueDateViewData.fixture(
selectedDateMillis = dueDate.toEpochMilliseconds(),
@@ -123,23 +126,21 @@ class CreateTaskViewModelTest :
saveButtonEnabled = true,
)
}
- }
- }
- @Test
- fun `DueDateChanged - should update dueDate`() = test {
- val dueDateMillis = 1_752_996_000_000
+ viewModel.onEvent(CreateTaskEvent.SaveClicked)
+ awaitState().saveButtonEnabled shouldBe false
- viewModel.onEvent(CreateTaskEvent.DueDateChanged(dueDateMillis))
+ taskCapture.get().also { task ->
+ task.priority shouldBe Priority.HIGHEST
+ task.dueDate shouldBe endOfDayDueDate
+ }
- awaitState().dueDate.also {
- it?.selectedDateMillis shouldBe dueDateMillis
- it?.selectedDateDisplayText shouldNotBe null
+ awaitSideEffect() shouldBe CreateTaskAction.NavigateBack
}
}
@Test
- fun `DueDateChanged - null should clear dueDate`() = test {
+ fun `DueDateChanged - should update and clear dueDate`() = test {
val dueDateMillis = 1_752_996_000_000
val expectedDueDate = CreateTaskViewData.DueDateViewData.fixture(
selectedDateMillis = dueDateMillis,
@@ -166,14 +167,6 @@ class CreateTaskViewModelTest :
every(mockTimeProvider::now) returns creationDate
everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
- currentState shouldBe CreateTaskViewData.fixture(
- title = "",
- description = null,
- dueDate = null,
- minDueDateMillis = startOfToday.toEpochMilliseconds(),
- saveButtonEnabled = false,
- )
-
viewModel.onEvent(CreateTaskEvent.TitleChanged(title))
expectState { copy(title = title, saveButtonEnabled = true) }
@@ -198,13 +191,14 @@ class CreateTaskViewModelTest :
}
@Test
- fun `SaveClicked - should persist default medium priority`() = test {
+ fun `SaveClicked - should persist task with default values`() = test {
val id = Uuid.random()
val title = "Task title"
+ val creationDate = Instant.parse("2026-07-21T12:00:00Z")
val taskCapture = Capture.slot()
every(mockUuidProvider::random) returns id
- every(mockTimeProvider::now) returns Instant.parse("2026-07-21T12:00:00Z")
+ every(mockTimeProvider::now) returns creationDate
everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
viewModel.onEvent(CreateTaskEvent.TitleChanged(title))
@@ -213,7 +207,17 @@ class CreateTaskViewModelTest :
viewModel.onEvent(CreateTaskEvent.SaveClicked)
awaitState().saveButtonEnabled shouldBe false
- taskCapture.get().priority shouldBe Priority.MEDIUM
+ taskCapture.get() shouldBe Task(
+ id = id,
+ title = title,
+ description = null,
+ creationDate = creationDate,
+ dueDate = null,
+ completionDate = null,
+ priority = Priority.MEDIUM,
+ url = null,
+ location = null,
+ )
}
@Test
@@ -238,44 +242,6 @@ class CreateTaskViewModelTest :
taskCapture.get().priority shouldBe Priority.BLOCKER
}
- @Test
- fun `should preserve saved task priority on init`() {
- val taskId = Uuid.random()
- val savedTask = Task.fixture(id = taskId, priority = Priority.HIGHEST, dueDate = null)
- val taskCapture = Capture.slot()
-
- everySuspend { mockTaskRepository.findById(taskId) } returns savedTask
- every(mockTimeProvider::now) returns Instant.parse("2026-07-22T12:00:00Z")
- everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit
-
- viewModel = CreateTaskViewModel(
- taskId = taskId.toString(),
- taskRepository = mockTaskRepository,
- timeProvider = mockTimeProvider,
- uuidProvider = mockUuidProvider,
- )
-
- test {
- expectState {
- copy(
- id = taskId.toString(),
- title = savedTask.title,
- description = savedTask.description,
- priority = Priority.HIGHEST,
- dueDate = null,
- saveButtonEnabled = true,
- )
- }
-
- viewModel.onEvent(CreateTaskEvent.SaveClicked)
- awaitState().saveButtonEnabled shouldBe false
-
- taskCapture.get().priority shouldBe Priority.HIGHEST
-
- awaitSideEffect() shouldBe CreateTaskAction.NavigateBack
- }
- }
-
@Test
fun `SaveClicked - should keep creationDate when editing an existing task`() {
val taskId = Uuid.random()
From cd84dd24504467886f23f6c16ea68fc7c1cb7f62 Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:40:09 +0100
Subject: [PATCH 11/14] string resource isntead of hardcoded
---
.../feature/createtask/FoliaryPriorityPickerTest.kt | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt
index 56c2417..087f819 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/FoliaryPriorityPickerTest.kt
@@ -10,6 +10,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest
import dev.appoutlet.foliary.data.task.database.entity.Priority
import foliary.foliary.generated.resources.Res
+import foliary.foliary.generated.resources.create_task_priority_label
import foliary.foliary.generated.resources.priority_pill_blocker
import foliary.foliary.generated.resources.priority_pill_high
import foliary.foliary.generated.resources.priority_pill_highest
@@ -18,6 +19,7 @@ import foliary.foliary.generated.resources.priority_pill_lowest
import foliary.foliary.generated.resources.priority_pill_medium
import io.kotest.matchers.shouldBe
import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.stringResource
import kotlin.test.Test
@OptIn(ExperimentalTestApi::class)
@@ -25,8 +27,7 @@ class FoliaryPriorityPickerTest {
@Test
fun `should render selected priority as pill`() = runComposeUiTest {
- val label = "Priority"
- val mediumText = getString(Res.string.priority_pill_medium)
+ val label = getString(Res.string.create_task_priority_label)
setContent {
FoliaryPriorityPicker(
@@ -37,7 +38,7 @@ class FoliaryPriorityPickerTest {
}
onNodeWithText(label).assertIsDisplayed()
- onNodeWithText(mediumText).assertIsDisplayed()
+ onNodeWithText(getString(Res.string.priority_pill_medium)).assertIsDisplayed()
onNodeWithTag("FoliaryPriorityPicker:Value", useUnmergedTree = true).assertIsDisplayed()
}
@@ -47,7 +48,7 @@ class FoliaryPriorityPickerTest {
FoliaryPriorityPicker(
selectedPriority = Priority.MEDIUM,
onPrioritySelected = {},
- label = "Priority"
+ label = stringResource(Res.string.create_task_priority_label)
)
}
@@ -62,7 +63,7 @@ class FoliaryPriorityPickerTest {
FoliaryPriorityPicker(
selectedPriority = Priority.MEDIUM,
onPrioritySelected = {},
- label = "Priority"
+ label = stringResource(Res.string.create_task_priority_label)
)
}
@@ -85,7 +86,7 @@ class FoliaryPriorityPickerTest {
FoliaryPriorityPicker(
selectedPriority = Priority.MEDIUM,
onPrioritySelected = { selectedPriority = it },
- label = "Priority"
+ label = stringResource(Res.string.create_task_priority_label)
)
}
From 45803b376c1dc29e1f6eb5b20fd7536a44590347 Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 11:42:00 +0100
Subject: [PATCH 12/14] chore(deps): downgrade kotlin from 2.4.21 to 2.4.20
---
gradle/libs.versions.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b4322ab..e5c4dc7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,7 +13,7 @@ koin = "4.2.2"
koin-compiler = "1.2.1"
kotest = "6.2.5"
ktor = "3.6.0"
-kotlin = "2.4.21"
+kotlin = "2.4.20"
kotlinx-coroutines = "1.11.0"
kotlinx-serialization = "1.11.0"
ksp = "2.3.12"
From 952ddd90768f797cbac71188b47431f71a4e9bde Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 12:05:39 +0100
Subject: [PATCH 13/14] run only jvm tests
---
.github/workflows/pull-request.yml | 2 +-
.../DefaultAuthenticationRepositoryTest.android.kt | 6 ++++++
.../core/ui/component/button/FoliaryPrimaryButtonTest.kt | 2 +-
.../core/ui/component/button/FoliarySecondaryButtonTest.kt | 2 +-
.../authentication/DefaultAuthenticationRepositoryTest.kt | 5 +++--
.../DefaultAuthenticationRepositoryTest.ios.kt | 5 +++++
.../DefaultAuthenticationRepositoryTest.jvm.kt | 5 +++++
gradle/libs.versions.toml | 2 +-
8 files changed, 23 insertions(+), 6 deletions(-)
create mode 100644 foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt
create mode 100644 foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt
create mode 100644 foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index b120bbe..a411a4d 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -36,7 +36,7 @@ jobs:
run: ./gradlew detekt
- name: Check code test coverage
- run: ./gradlew allTests -Pkover koverVerify
+ run: ./gradlew jvmTest -Pkover koverVerify
- name: Upload JVM test report
if: always()
diff --git a/foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt b/foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt
new file mode 100644
index 0000000..83db01d
--- /dev/null
+++ b/foliary/src/androidHostTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.android.kt
@@ -0,0 +1,6 @@
+package dev.appoutlet.foliary.data.authentication
+
+import eu.anifantakis.lib.ksafe.KSafe
+import kotlin.io.path.createTempDirectory
+
+actual fun platformKSafe(): KSafe = error("")
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt
index cdb2f2b..7b2b2f5 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt
@@ -17,7 +17,7 @@ import kotlin.test.Test
class FoliaryPrimaryButtonTest {
@Test
- fun `should show content, be enabled by default and invoke onClick when clicked`() = runComposeUiTest {
+ fun `should show content be enabled by default and invoke onClick when clicked`() = runComposeUiTest {
var clicked = false
val testTag = "PrimaryButton"
val buttonText = "Click Me"
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt
index d629684..dc07aab 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt
@@ -17,7 +17,7 @@ import kotlin.test.Test
class FoliarySecondaryButtonTest {
@Test
- fun `should show content, be enabled by default and invoke onClick when clicked`() = runComposeUiTest {
+ fun `should show content be enabled by default and invoke onClick when clicked`() = runComposeUiTest {
var clicked = false
val testTag = "SecondaryButton"
val buttonText = "Click Me"
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt
index cebefc3..4eb77ba 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt
@@ -18,12 +18,11 @@ import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
-import kotlin.io.path.createTempDirectory
import kotlin.test.Test
class DefaultAuthenticationRepositoryTest {
private val mockAuth = mock(mode = MockMode.autoUnit)
- private val ksafe = KSafe(baseDir = createTempDirectory("ksafe-test").toFile())
+ private val ksafe = platformKsafe()
val subject = DefaultAuthenticationRepository(
lazyAuth = lazy { mockAuth },
@@ -116,3 +115,5 @@ class DefaultAuthenticationRepositoryTest {
subject.currentUser() shouldBe fixture
}
}
+
+expect fun platformKSafe(): KSafe
diff --git a/foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt b/foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt
new file mode 100644
index 0000000..c73788d
--- /dev/null
+++ b/foliary/src/iosTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.ios.kt
@@ -0,0 +1,5 @@
+package dev.appoutlet.foliary.data.authentication
+
+import eu.anifantakis.lib.ksafe.KSafe
+
+actual fun platformKSafe() = KSafe()
diff --git a/foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt b/foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt
new file mode 100644
index 0000000..c73788d
--- /dev/null
+++ b/foliary/src/jvmTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.jvm.kt
@@ -0,0 +1,5 @@
+package dev.appoutlet.foliary.data.authentication
+
+import eu.anifantakis.lib.ksafe.KSafe
+
+actual fun platformKSafe() = KSafe()
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e5c4dc7..d6a039c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -87,7 +87,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
-ksafe = "eu.anifantakis:ksafe:3.2.0"
+ksafe = "eu.anifantakis:ksafe:3.3.0"
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
From d05a7601aaae2d65936bfa9027906aff5e56ca4c Mon Sep 17 00:00:00 2001
From: Messias Junior
Date: Tue, 20 Oct 2026 12:06:38 +0100
Subject: [PATCH 14/14] fix method call
---
.../data/authentication/DefaultAuthenticationRepositoryTest.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt
index 4eb77ba..04d89ff 100644
--- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt
+++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/authentication/DefaultAuthenticationRepositoryTest.kt
@@ -22,7 +22,7 @@ import kotlin.test.Test
class DefaultAuthenticationRepositoryTest {
private val mockAuth = mock(mode = MockMode.autoUnit)
- private val ksafe = platformKsafe()
+ private val ksafe = platformKSafe()
val subject = DefaultAuthenticationRepository(
lazyAuth = lazy { mockAuth },