Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/implement UI for event filtering #224

Merged
merged 28 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9a591b0
fix(ui): implement new action in topAppBar for park filtering
Simmanz Dec 11, 2024
d6b4dca
fix: implement basic filtering logic
Simmanz Dec 12, 2024
c8d7a6a
fix(mistake): first branch commit had older local changes from other …
Simmanz Dec 12, 2024
2b1c50f
feat(ui): implement park filter ui for user interaction
Simmanz Dec 12, 2024
c5840c4
fix: park medium threshold was overlapping with high
Simmanz Dec 12, 2024
fcba30f
chore: remove unused import
Simmanz Dec 12, 2024
6fa46fc
feat: implement event MVVM link to fetch events of a park for filtering
Simmanz Dec 12, 2024
8c8b04c
fix(ui): prevent icons from blinking
Simmanz Dec 12, 2024
4cea14a
fix(ui): really prevent icons from blinking
Simmanz Dec 12, 2024
a377c6d
fix(mvvm): add new call to try and fix eventList calls
Simmanz Dec 12, 2024
8c0e224
fix: correct filtering functions (eventStatus, eventFull)
Simmanz Dec 12, 2024
8b0ea3a
Merge branch 'main' of https://github.com/SwEnt-Group8/Street-work-ap…
Simmanz Dec 12, 2024
f6a4e2e
fix: might be better to have getOrCreateAllParks invokation before th…
Simmanz Dec 12, 2024
d39c847
Fix: remove problematic features and adapt ui + logic
Simmanz Dec 16, 2024
ef3523a
Fix: small fix in text value and parameter
Simmanz Dec 16, 2024
c1843b8
Fix: add again the ability to not precise arguments for set function
Simmanz Dec 16, 2024
1195644
Feat(test): implement unit test for FilterSettings API
Simmanz Dec 17, 2024
c291af1
Feat(test): implement unit test for ParkFilter API
Simmanz Dec 17, 2024
8bed3f8
chore(format): apply ktfmt formatting
Simmanz Dec 17, 2024
a5ea983
Feat(ui): add test tags to ParkFilter ui
Simmanz Dec 17, 2024
39928fa
Feat: added comment explaining why Theme.kt is not used
Simmanz Dec 17, 2024
ae4f4bf
Feat(ui): implement strings.xml link for filter UI
Simmanz Dec 17, 2024
c2bbc04
Feat(test): implement new UI tests for filtering
Simmanz Dec 17, 2024
25d5077
Fix: delete unused threshold function
Simmanz Dec 17, 2024
7bba4ec
Fix: set default rating setting as constant
Simmanz Dec 17, 2024
bc5c0f0
Fix: remove older changes that are no longer relevant
Simmanz Dec 17, 2024
a7a7a75
Fix(test): replace repetitive calls in a @Before function
Simmanz Dec 17, 2024
d032ba4
Feat(ui): change event density categories to their ranges
Simmanz Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.android.streetworkapp.ui.map

import android.content.Context
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.android.sample.R
import com.android.streetworkapp.utils.EventDensity
import com.android.streetworkapp.utils.FilterSettings
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class ParkFilterTest {
@get:Rule val composeTestRule = createComposeRule()

private lateinit var filterSettings: FilterSettings
private lateinit var context: Context

@Before
fun setUp() {
filterSettings = FilterSettings()
composeTestRule.setContent {
ParkFilterSettings(filterSettings)
context = LocalContext.current
}
}

@Test
fun displayAllComponents() {
composeTestRule.onNodeWithTag("ratingFilterTitle").assertExists()
composeTestRule.onNodeWithTag("ratingComponent").assertExists()
composeTestRule.onNodeWithTag("eventDensityFilterTitle").assertExists()
composeTestRule.onNodeWithTag("lowDensityFilterChip").assertExists()
composeTestRule.onNodeWithTag("mediumDensityFilterChip").assertExists()
composeTestRule.onNodeWithTag("highDensityFilterChip").assertExists()
composeTestRule.onNodeWithTag("resetButton").assertExists()

// Check the default values being correctly displayed
composeTestRule
.onNodeWithTag("ratingFilterTitle")
.assertTextEquals(
context.getString(R.string.rating_filter_title, FilterSettings.DEFAULT_RATING))
composeTestRule
.onNodeWithTag("eventDensityFilterTitle")
.assertTextEquals(context.getString(R.string.eventDensity_filter_title))

// Chips all enabled by default
composeTestRule.onNodeWithTag("lowDensityFilterChip").assertIsSelected()
composeTestRule.onNodeWithTag("mediumDensityFilterChip").assertIsSelected()
composeTestRule.onNodeWithTag("highDensityFilterChip").assertIsSelected()
}

@Test
fun isEventFilterInteractionCorrect() {

assert(filterSettings.eventDensity.size == 3)

// Click on the chips to change the filter settings :
composeTestRule.onNodeWithTag("lowDensityFilterChip").performClick()
composeTestRule.onNodeWithTag("mediumDensityFilterChip").performClick()
composeTestRule.onNodeWithTag("highDensityFilterChip").performClick()

composeTestRule.waitForIdle()

// Check if the chips are selected :
composeTestRule.onNodeWithTag("lowDensityFilterChip").assertIsNotSelected()
composeTestRule.onNodeWithTag("mediumDensityFilterChip").assertIsNotSelected()
composeTestRule.onNodeWithTag("highDensityFilterChip").assertIsNotSelected()

assert(filterSettings.eventDensity.size == 0)

composeTestRule.onNodeWithTag("lowDensityFilterChip").performClick()

composeTestRule.waitForIdle()

assert(filterSettings.eventDensity.contains(EventDensity.LOW))
assert(!filterSettings.eventDensity.contains(EventDensity.MEDIUM))
assert(!filterSettings.eventDensity.contains(EventDensity.HIGH))

composeTestRule.onNodeWithTag("mediumDensityFilterChip").performClick()

composeTestRule.waitForIdle()

assert(filterSettings.eventDensity.contains(EventDensity.LOW))
assert(filterSettings.eventDensity.contains(EventDensity.MEDIUM))
assert(!filterSettings.eventDensity.contains(EventDensity.HIGH))

composeTestRule.onNodeWithTag("highDensityFilterChip").performClick()

composeTestRule.waitForIdle()

assert(filterSettings.eventDensity.contains(EventDensity.LOW))
assert(filterSettings.eventDensity.contains(EventDensity.MEDIUM))
assert(filterSettings.eventDensity.contains(EventDensity.HIGH))

// Click on the reset button :
composeTestRule.onNodeWithTag("resetButton").performClick()

composeTestRule.waitForIdle()

// Check if the chips are selected :
composeTestRule.onNodeWithTag("lowDensityFilterChip").assertIsSelected()
composeTestRule.onNodeWithTag("mediumDensityFilterChip").assertIsSelected()
composeTestRule.onNodeWithTag("highDensityFilterChip").assertIsSelected()
}
}
11 changes: 10 additions & 1 deletion app/src/main/java/com/android/streetworkapp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ fun StreetWorkApp(
route = Route.MAP,
) {
composable(Screen.MAP) {
val showFilterSettings = mutableStateOf(false)

infoManager.Display(LocalContext.current)
MapScreen(
parkLocationViewModel,
Expand All @@ -349,11 +351,18 @@ fun StreetWorkApp(
mapCallbackOnMapLoaded,
innerPadding,
scope,
host)
host,
showFilterSettings)

screenParams?.topAppBarManager?.setActionCallback(
TopAppBarManager.TopAppBarAction.SEARCH) {
showSearchBar.value = true
}

screenParams?.topAppBarManager?.setActionCallback(
TopAppBarManager.TopAppBarAction.FILTER) {
showFilterSettings.value = true
}
}
composable(Screen.PARK_OVERVIEW) {
infoManager.Display(LocalContext.current)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ open class EventViewModel(private val repository: EventRepository) : ViewModel()
val currentEvent: StateFlow<Event?>
get() = _currentEvent

private val _eventList: MutableStateFlow<Map<String, Event>> = MutableStateFlow(emptyMap())
val eventList: StateFlow<Map<String, Event>>
get() = _eventList

fun setCurrentEvent(event: Event?) {
_currentEvent.value = event
}
Expand All @@ -31,6 +35,33 @@ open class EventViewModel(private val repository: EventRepository) : ViewModel()
}
}

/**
* Get a list of events by their IDs.
*
* @param eids The list of event IDs.
*/
fun getEventsByEid(eids: List<String>) {
val fetchedEvents = mutableMapOf<String, Event>()
viewModelScope.launch {
eids.forEach {
val event = repository.getEventByEid(it)
if (event != null) fetchedEvents[it] = event
}
_eventList.value = fetchedEvents
}
}

/**
* Get a list of events from the park they are in.
*
* @param parks The list of parks.
*/
fun getEventsByParkList(parks: List<Park>) {
val eids = parks.flatMap { it.events }

getEventsByEid(eids)
}

fun setUiState(state: EventOverviewUiState) {
_uiState.value = state
}
Expand Down
99 changes: 98 additions & 1 deletion app/src/main/java/com/android/streetworkapp/ui/map/MapUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Event
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SearchBar
Expand All @@ -41,15 +45,23 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.navigation.compose.rememberNavController
import com.android.sample.R
import com.android.streetworkapp.model.park.Park
import com.android.streetworkapp.model.park.ParkViewModel
import com.android.streetworkapp.model.parklocation.ParkLocationViewModel
import com.android.streetworkapp.model.user.UserViewModel
import com.android.streetworkapp.ui.navigation.NavigationActions
import com.android.streetworkapp.ui.navigation.Screen
import com.android.streetworkapp.ui.park.InteractiveRatingComponent
import com.android.streetworkapp.ui.park.RatingComponent
import com.android.streetworkapp.ui.theme.ColorPalette
import com.android.streetworkapp.ui.theme.Typography as Type
import com.android.streetworkapp.ui.utils.CustomDialog
import com.android.streetworkapp.ui.utils.DialogType
import com.android.streetworkapp.utils.EventDensity
import com.android.streetworkapp.utils.FilterSettings
import com.android.streetworkapp.utils.LocationService
import com.android.streetworkapp.utils.ParkFilter
import com.android.streetworkapp.utils.PermissionManager
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
Expand All @@ -68,7 +80,7 @@ import kotlinx.coroutines.CoroutineScope
* @param parkLocationViewModel The view model for park locations.
* @param navigationActions The navigation actions to navigate to other screens.
*/
@OptIn(MapsComposeExperimentalApi::class)
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MapScreen(
parkLocationViewModel: ParkLocationViewModel,
Expand All @@ -80,6 +92,7 @@ fun MapScreen(
innerPaddingValues: PaddingValues = PaddingValues(0.dp),
scope: CoroutineScope = rememberCoroutineScope(),
host: SnackbarHostState? = null,
showFilterSettings: MutableState<Boolean> = mutableStateOf(false)
) {

val context = LocalContext.current
Expand Down Expand Up @@ -114,6 +127,11 @@ fun MapScreen(

LaunchedEffect(parks) { parkViewModel.getOrCreateAllParksByLocation(parks) }

// Set values for park filtering :
val filter = FilterSettings()
val parkFilter = ParkFilter(filter)
val userFilterInput = FilterSettings()

Box(modifier = Modifier.testTag("mapScreen")) {
// Create a CameraPositionState to control the camera position
val cameraPositionState = rememberCameraPositionState {
Expand Down Expand Up @@ -142,6 +160,7 @@ fun MapScreen(
parkList.value
.filterNotNull()
.filter { it.name.contains(searchQuery.value, ignoreCase = true) }
.filter { parkFilter.filter(it) }
.forEach { park ->
++markerIndex

Expand Down Expand Up @@ -169,6 +188,84 @@ fun MapScreen(
}
}
}
// Settings variable defined beforehand :
// Affected by the filter settings => changes confirmed when confirming (onSubmit).

// Display the Filter component :
CustomDialog(
showFilterSettings,
tag = "Filter",
dialogType = DialogType.CONFIRM,
title = LocalContext.current.getString(R.string.park_filter_title),
Content = { ParkFilterSettings(userFilterInput) },
onSubmit = { filter.set(userFilterInput) },
onDismiss = { userFilterInput.set(filter) })
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ParkFilterSettings(userFilterInput: FilterSettings) {
val context = LocalContext.current

// Note - This is a composable and cannot be defined in the ColorPalette (Theme.kt).
val filterChipColors =
FilterChipDefaults.filterChipColors(
selectedLabelColor = ColorPalette.PRINCIPLE_BACKGROUND_COLOR,
selectedContainerColor = ColorPalette.INTERACTION_COLOR_DARK)

Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
// Park rating filter :
Text(
text =
context.getString(
R.string.rating_filter_title, userFilterInput.minRating.value.toString()),
fontSize = Type.bodyLarge.fontSize,
modifier = Modifier.testTag("ratingFilterTitle"))
InteractiveRatingComponent(userFilterInput.minRating)

HorizontalDivider()

// Event quantity filter :
Text(
context.getString(R.string.eventDensity_filter_title),
fontSize = Type.bodyLarge.fontSize,
modifier = Modifier.testTag("eventDensityFilterTitle"))

Row(modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth(0.825f)) {
FilterChip(
selected = userFilterInput.eventDensity.contains(EventDensity.LOW),
onClick = { userFilterInput.updateDensity(EventDensity.LOW) },
label = { Text("[ 0 .. 2 ]") },
colors = filterChipColors,
modifier = Modifier.padding(end = 2.dp).testTag("lowDensityFilterChip"))

FilterChip(
selected = userFilterInput.eventDensity.contains(EventDensity.MEDIUM),
onClick = { userFilterInput.updateDensity(EventDensity.MEDIUM) },
label = { Text("[ 3 .. 6 ]") },
colors = filterChipColors,
modifier = Modifier.padding(end = 2.dp).testTag("mediumDensityFilterChip"))

FilterChip(
selected = userFilterInput.eventDensity.contains(EventDensity.HIGH),
onClick = { userFilterInput.updateDensity(EventDensity.HIGH) },
label = { Text("[7 + ]") },
colors = filterChipColors,
modifier = Modifier.padding(end = 2.dp).testTag("highDensityFilterChip"))
}

HorizontalDivider()

Button(
onClick = { userFilterInput.reset() },
colors = ColorPalette.BUTTON_COLOR,
modifier =
Modifier.align(Alignment.CenterHorizontally)
.padding(top = 4.dp)
.testTag("resetButton")) {
Text(context.getString(R.string.reset_button))
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ data class ScreenParams(
val bottomBarType: BottomNavigationMenuType = BottomNavigationMenuType.DEFAULT,
val isTopBarVisible: Boolean = true,
val topAppBarManager: TopAppBarManager?,
val hasSearchBar: Boolean = false
val hasSearchBar: Boolean = false,
) {
companion object {
val AUTH =
Expand All @@ -65,7 +65,8 @@ data class ScreenParams(
actions =
listOf(
TopAppBarManager.TopAppBarAction.SEARCH,
TopAppBarManager.TopAppBarAction.INFO)),
TopAppBarManager.TopAppBarAction.INFO,
TopAppBarManager.TopAppBarAction.FILTER)),
hasSearchBar = true)
val PROFILE =
ScreenParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class TopAppBarManager(
icon = R.drawable.settings, contentDescription = "Settings", testTag = "settingsButton"),
INFO(icon = R.drawable.octagon_help, contentDescription = "Info", testTag = "infoButton"),
SEARCH(icon = R.drawable.map_search, contentDescription = "Search", testTag = "searchButton"),
FILTER(icon = R.drawable.map_search, contentDescription = "Filter", testTag = "filterButton"),

// Add more actions here
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ fun InteractiveRatingComponent(rating: MutableState<Int>) {
* @param navigationActions The navigation actions to navigate to other screens.
*/
@Composable
fun EventItemList(eventViewModel: EventViewModel, navigationActions: NavigationActions) {
fun EventItemList(
eventViewModel: EventViewModel,
navigationActions: NavigationActions,
) {
val uiState = eventViewModel.uiState.collectAsState().value
misterM125 marked this conversation as resolved.
Show resolved Hide resolved

Column(modifier = Modifier.testTag("eventItemList")) {
Expand Down
Loading
Loading