Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
Search Functionality In Category Screen (#3499)
Browse files Browse the repository at this point in the history
* Under dev

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488

* fix-issue-3488
  • Loading branch information
shamim-emon authored Sep 13, 2024
1 parent 6231cf5 commit 8308d88
Show file tree
Hide file tree
Showing 16 changed files with 209 additions and 17 deletions.
147 changes: 144 additions & 3 deletions screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.ivy.legacy.IvyWalletPreview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -51,9 +52,11 @@ import com.ivy.data.model.primitive.IconAsset
import com.ivy.data.model.primitive.NotBlankTrimmedString
import com.ivy.design.l0_system.UI
import com.ivy.design.l0_system.style
import com.ivy.legacy.ui.SearchInput
import com.ivy.legacy.utils.balancePrefix
import com.ivy.legacy.utils.compactBalancePrefix
import com.ivy.legacy.utils.format
import com.ivy.legacy.utils.selectEndTextFieldValue
import com.ivy.navigation.CategoriesScreen
import com.ivy.navigation.TransactionsScreen
import com.ivy.navigation.navigation
Expand Down Expand Up @@ -100,7 +103,10 @@ fun BoxWithConstraintsScope.CategoriesScreen(screen: CategoriesScreen) {

@Composable
private fun BoxWithConstraintsScope.UI(
state: CategoriesScreenState = CategoriesScreenState(compactCategoriesModeEnabled = false),
state: CategoriesScreenState = CategoriesScreenState(
compactCategoriesModeEnabled = false,
showCategorySearchBar = false
),
onEvent: (CategoriesScreenEvent) -> Unit = {}
) {
val nav = navigation()
Expand Down Expand Up @@ -158,6 +164,10 @@ private fun BoxWithConstraintsScope.UI(
Spacer(Modifier.width(24.dp))
}

if (state.showCategorySearchBar) {
Spacer(Modifier.height(16.dp))
SearchField(onSearch = { onEvent(CategoriesScreenEvent.OnSearchQueryUpdate(it)) })
}
Spacer(Modifier.height(16.dp))
}

Expand Down Expand Up @@ -679,11 +689,102 @@ private fun PreviewCategoriesCompactModeEnabled(theme: Theme = Theme.LIGHT) {

@Preview
@Composable
private fun Preview(theme: Theme = Theme.LIGHT, compactModeEnabled: Boolean = false) {
com.ivy.legacy.IvyWalletPreview(theme) {
private fun PreviewCategoriesCompactModeEnabledAndSearchBarEnabled(theme: Theme = Theme.LIGHT) {
Preview(theme = theme, compactModeEnabled = true, displaySearchBarEnabled = true)
}

@Preview
@Composable
private fun Preview(
theme: Theme = Theme.LIGHT,
compactModeEnabled: Boolean = false,
displaySearchBarEnabled: Boolean = false
) {
IvyWalletPreview(theme) {
val state = CategoriesScreenState(
baseCurrency = "BGN",
compactCategoriesModeEnabled = compactModeEnabled,
showCategorySearchBar = displaySearchBarEnabled,
categories = persistentListOf(
CategoryData(
category = Category(
id = CategoryId(UUID.randomUUID()),
name = NotBlankTrimmedString.unsafe("Groceries"),
color = ColorInt(Green.toArgb()),
icon = IconAsset.unsafe("groceries"),
orderNum = 0.0,
),
monthlyBalance = 2125.0,
monthlyExpenses = 920.0,
monthlyIncome = 3045.0
),
CategoryData(
category = Category(
id = CategoryId(UUID.randomUUID()),
name = NotBlankTrimmedString.unsafe("Fun"),
color = ColorInt(Orange.toArgb()),
icon = IconAsset.unsafe("game"),
orderNum = 0.0,
),
monthlyBalance = 1200.0,
monthlyExpenses = 750.0,
monthlyIncome = 0.0
),
CategoryData(
category = Category(
id = CategoryId(UUID.randomUUID()),
name = NotBlankTrimmedString.unsafe("Ivy"),
color = ColorInt(IvyDark.toArgb()),
icon = IconAsset.unsafe("star"),
orderNum = 0.0,
),
monthlyBalance = 1200.0,
monthlyExpenses = 0.0,
monthlyIncome = 5000.0
),
CategoryData(
category = Category(
id = CategoryId(UUID.randomUUID()),
name = NotBlankTrimmedString.unsafe("Food"),
color = ColorInt(GreenLight.toArgb()),
icon = IconAsset.unsafe("atom"),
orderNum = 0.0,
),
monthlyBalance = 12125.21,
monthlyExpenses = 1350.50,
monthlyIncome = 8000.48
),
CategoryData(
category = Category(
id = CategoryId(UUID.randomUUID()),
name = NotBlankTrimmedString.unsafe("Shisha"),
color = ColorInt(GreenDark.toArgb()),
icon = IconAsset.unsafe("drink"),
orderNum = 0.0,
),
monthlyBalance = 820.0,
monthlyExpenses = 340.0,
monthlyIncome = 400.0
),

)
)
UI(state = state)
}
}

@Preview
@Composable
private fun PreviewWithSearchBarEnabled(
theme: Theme = Theme.LIGHT,
compactModeEnabled: Boolean = false,
displaySearchBarEnabled: Boolean = true
) {
IvyWalletPreview(theme) {
val state = CategoriesScreenState(
baseCurrency = "BGN",
compactCategoriesModeEnabled = compactModeEnabled,
showCategorySearchBar = displaySearchBarEnabled,
categories = persistentListOf(
CategoryData(
category = Category(
Expand Down Expand Up @@ -752,6 +853,26 @@ private fun Preview(theme: Theme = Theme.LIGHT, compactModeEnabled: Boolean = fa
}
}

@Composable
private fun SearchField(
onSearch: (String) -> Unit,
) {
var searchQueryTextFieldValue by remember {
mutableStateOf(selectEndTextFieldValue(""))
}

SearchInput(
searchQueryTextFieldValue = searchQueryTextFieldValue,
hint = "Search categories",
focus = false,
showClearIcon = searchQueryTextFieldValue.text.isNotEmpty(),
onSetSearchQueryTextField = {
searchQueryTextFieldValue = it
onSearch(it.text)
}
)
}

/** For screenshot testing */
@Composable
fun CategoriesScreenUiTest(isDark: Boolean) {
Expand All @@ -762,6 +883,16 @@ fun CategoriesScreenUiTest(isDark: Boolean) {
Preview(theme)
}

/** For screenshot testing */
@Composable
fun CategoriesScreenWithSearchBarUiTest(isDark: Boolean) {
val theme = when (isDark) {
true -> Theme.DARK
false -> Theme.LIGHT
}
Preview(theme = theme, displaySearchBarEnabled = true)
}

/** For screenshot testing */
@Composable
fun CategoriesScreenCompactUiTest(isDark: Boolean) {
Expand All @@ -770,4 +901,14 @@ fun CategoriesScreenCompactUiTest(isDark: Boolean) {
false -> Theme.LIGHT
}
Preview(theme, compactModeEnabled = true)
}

/** For screenshot testing */
@Composable
fun CategoriesScreenWithSearchBarCompactUiTest(isDark: Boolean) {
val theme = when (isDark) {
true -> Theme.DARK
false -> Theme.LIGHT
}
Preview(theme, compactModeEnabled = true, displaySearchBarEnabled = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ sealed interface CategoriesScreenEvent {
data class OnSortOrderModalVisible(val visible: Boolean) : CategoriesScreenEvent
data class OnCategoryModalVisible(val categoryModalData: CategoryModalData?) :
CategoriesScreenEvent
data class OnSearchQueryUpdate(val queryString: String) : CategoriesScreenEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ data class CategoriesScreenState(
val sortOrderItems: ImmutableList<SortOrder> = SortOrder.values().toList().toImmutableList(),
val sortOrder: SortOrder = SortOrder.DEFAULT,
val compactCategoriesModeEnabled: Boolean,
val showCategorySearchBar: Boolean,

)
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.ivy.base.legacy.SharedPrefs
import com.ivy.base.legacy.Transaction
import com.ivy.base.time.TimeConverter
import com.ivy.base.time.TimeProvider
import com.ivy.ui.ComposeViewModel
import com.ivy.data.repository.CategoryRepository
import com.ivy.domain.features.Features
import com.ivy.frp.action.thenMap
import com.ivy.frp.thenInvokeAfter
import com.ivy.legacy.data.model.TimePeriod
import com.ivy.legacy.datamodel.Account
import com.ivy.legacy.utils.ioThread
import com.ivy.ui.ComposeViewModel
import com.ivy.wallet.domain.action.account.AccountsAct
import com.ivy.wallet.domain.action.category.LegacyCategoryIncomeWithAccountFiltersAct
import com.ivy.wallet.domain.action.settings.BaseCurrencyAct
Expand Down Expand Up @@ -55,6 +56,7 @@ class CategoriesViewModel @Inject constructor(
private val baseCurrency = mutableStateOf("")
private val categories =
mutableStateOf<ImmutableList<CategoryData>>(persistentListOf<CategoryData>())
private val searchQuery = mutableStateOf("")
private val reorderModalVisible = mutableStateOf(false)
private val categoryModalData = mutableStateOf<CategoryModalData?>(null)
private val sortModalVisible = mutableStateOf(false)
Expand All @@ -74,6 +76,7 @@ class CategoriesViewModel @Inject constructor(
sortOrder = getSortOrder(),
sortModalVisible = getSortModalVisible(),
compactCategoriesModeEnabled = getCompactCategoriesMode(),
showCategorySearchBar = getShowCategorySearchBar()
)
}

Expand All @@ -82,14 +85,24 @@ class CategoriesViewModel @Inject constructor(
return features.compactCategoriesMode.asEnabledState()
}

@Composable
private fun getShowCategorySearchBar(): Boolean {
return features.showCategorySearchBar.asEnabledState()
}

@Composable
private fun getBaseCurrency(): String {
return baseCurrency.value
}

@Composable
private fun getCategories(): ImmutableList<CategoryData> {
return categories.value
val allCats = categories.value
return remember(allCats, searchQuery.value) {
allCats.filter {
searchQuery.value.lowercase().trim() in it.category.name.toString().lowercase()
}.toImmutableList()
}
}

@Composable
Expand Down Expand Up @@ -126,7 +139,11 @@ class CategoriesViewModel @Inject constructor(
ioThread {
val range = TimePeriod.currentMonth(
startDayOfMonth = ivyContext.startDayOfMonth
).toRange(ivyContext.startDayOfMonth, timeConverter, timeProvider) // this must be monthly
).toRange(
ivyContext.startDayOfMonth,
timeConverter,
timeProvider
) // this must be monthly

allAccounts = accountsAct(Unit)
baseCurrency.value = baseCurrencyAct(Unit)
Expand All @@ -135,7 +152,7 @@ class CategoriesViewModel @Inject constructor(
TrnsWithRangeAndAccFiltersAct.Input(
range = range,
accountIdFilterSet = suspend { allAccounts } thenMap { it.id }
thenInvokeAfter { it.toHashSet() }
thenInvokeAfter { it.toHashSet() }
)
)

Expand Down Expand Up @@ -171,11 +188,14 @@ class CategoriesViewModel @Inject constructor(
}

val sortedList = sortList(categories, sortOrder.value).toImmutableList()

this.categories.value = sortedList
}
}

private fun updateSearchQuery(queryString: String) {
searchQuery.value = queryString
}

private suspend fun reorder(
newOrder: List<CategoryData>,
sortOrder: SortOrder = SortOrder.DEFAULT
Expand Down Expand Up @@ -244,6 +264,8 @@ class CategoriesViewModel @Inject constructor(
is CategoriesScreenEvent.OnCategoryModalVisible -> {
categoryModalData.value = event.categoryModalData
}

is CategoriesScreenEvent.OnSearchQueryUpdate -> updateSearchQuery(event.queryString)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,24 @@ class CategoriesScreenPaparazziTest(
}
}

@Test
fun `snapshot Categories nonCompact Screen with search bar`() {
snapshot(theme) {
CategoriesScreenWithSearchBarUiTest(theme == PaparazziTheme.Dark)
}
}

@Test
fun `snapshot Categories compact Screen`() {
snapshot(theme) {
CategoriesScreenCompactUiTest(theme == PaparazziTheme.Dark)
}
}
}

@Test
fun `snapshot Categories compact Screen with search bar`() {
snapshot(theme) {
CategoriesScreenWithSearchBarCompactUiTest(theme == PaparazziTheme.Dark)
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface Features {
val compactAccountsMode: BoolFeature
val compactCategoriesMode: BoolFeature
val showTitleSuggestions: BoolFeature
val showCategorySearchBar: BoolFeature
val hideTotalBalance: BoolFeature

val allFeatures: List<BoolFeature>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ class IvyFeatures @Inject constructor() : Features {
defaultValue = true
)

override val showCategorySearchBar = BoolFeature(
key = "search_categories",
name = "Search categories",
description = "Show search bar in category screen",
defaultValue = true
)

override val hideTotalBalance = BoolFeature(
key = "hide_total_balance",
name = "Hide total balance",
Expand All @@ -45,6 +52,7 @@ class IvyFeatures @Inject constructor() : Features {
compactAccountsMode,
compactCategoriesMode,
showTitleSuggestions,
showCategorySearchBar,
hideTotalBalance
)
}
Loading

1 comment on commit 8308d88

@Muthuraj5107220
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot_2024-09-13-16-41-45-06_25a5cbee76b0e8af12aa5b9c322cd4a3
Screenshot_2024-09-13-16-41-40-21_25a5cbee76b0e8af12aa5b9c322cd4a3
I tried the new feature of Category search which is working fine as expected

Please sign in to comment.