Add an empty list placeholder for MyLocationsList

This commit is contained in:
Nebojsa Vuksic 2025-07-31 14:24:49 +02:00
parent 88c1d4b4bd
commit e791d63d22
3 changed files with 154 additions and 2 deletions

View File

@ -7,7 +7,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified
@ -17,8 +19,11 @@ import org.jetbrains.jewel.foundation.lazy.items
import org.jetbrains.jewel.foundation.lazy.rememberSelectableLazyListState import org.jetbrains.jewel.foundation.lazy.rememberSelectableLazyListState
import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.plugins.template.ComposeTemplateBundle import org.jetbrains.plugins.template.ComposeTemplateBundle
import org.jetbrains.plugins.template.weatherApp.model.Location import org.jetbrains.plugins.template.weatherApp.model.Location
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
import org.jetbrains.plugins.template.weatherApp.services.LocationsProvider import org.jetbrains.plugins.template.weatherApp.services.LocationsProvider
import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi
@ -76,14 +81,58 @@ private fun LeftColumn(
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
MyLocationsList(Modifier.fillMaxSize(), myLocationsViewModelApi) MyLocationsListWithEmptyListPlaceholder(Modifier.fillMaxSize(), myLocationsViewModelApi)
} }
} }
@Composable @Composable
internal fun MyLocationsList(modifier: Modifier = Modifier, myLocationsViewModelApi: MyLocationsViewModelApi) { internal fun MyLocationsListWithEmptyListPlaceholder(
modifier: Modifier = Modifier,
myLocationsViewModelApi: MyLocationsViewModelApi
) {
val myLocations = myLocationsViewModelApi.myLocationsFlow.collectAsState(emptyList()).value val myLocations = myLocationsViewModelApi.myLocationsFlow.collectAsState(emptyList()).value
if (myLocations.isNotEmpty()) {
MyLocationList(myLocations, modifier, myLocationsViewModelApi)
} else {
EmptyListPlaceholder(modifier)
}
}
@Composable
private fun EmptyListPlaceholder(
modifier: Modifier,
placeholderText: String = ComposeTemplateBundle.message("weather.app.my.locations.empty.list.placeholder.text"),
placeholderIcon: IconKey = AllIconsKeys.Actions.AddList
) {
Column(
modifier = modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
placeholderIcon,
contentDescription = ComposeTemplateBundle.message("weather.app.my.locations.empty.list.placeholder.icon.content.description"),
Modifier.size(32.dp),
tint = Color.White
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = placeholderText,
style = JewelTheme.defaultTextStyle,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun MyLocationList(
myLocations: List<SelectableLocation>,
modifier: Modifier,
myLocationsViewModelApi: MyLocationsViewModelApi
) {
val listState = rememberSelectableLazyListState() val listState = rememberSelectableLazyListState()
// JEWEL-938 This will trigger on SelectableLazyColum's `onSelectedIndexesChange` callback // JEWEL-938 This will trigger on SelectableLazyColum's `onSelectedIndexesChange` callback
LaunchedEffect(myLocations) { LaunchedEffect(myLocations) {

View File

@ -2,6 +2,8 @@ weather.app.temperature.text={0}\u00B0C
weather.app.humidity.text=Humidity: {0}% weather.app.humidity.text=Humidity: {0}%
weather.app.wind.direction.text=Wind: {0} km/h {1} weather.app.wind.direction.text=Wind: {0} km/h {1}
weather.app.my.locations.header.text=My Locations weather.app.my.locations.header.text=My Locations
weather.app.my.locations.empty.list.placeholder.text=No locations added yet. Go and add the first location.
weather.app.my.locations.empty.list.placeholder.icon.content.description=Empty list icon.
weather.app.search.toolbar.menu.add.button.text=Add weather.app.search.toolbar.menu.add.button.text=Add
weather.app.search.toolbar.menu.add.button.content.description=Add a place to a watch list. weather.app.search.toolbar.menu.add.button.content.description=Add a place to a watch list.
weather.app.7days.forecast.title.text=7-day Forecast weather.app.7days.forecast.title.text=7-day Forecast

View File

@ -0,0 +1,101 @@
package org.jetbrains.plugins.template
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.test.runTest
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
import org.jetbrains.plugins.template.weatherApp.model.Location
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi
import org.jetbrains.plugins.template.weatherApp.ui.MyLocationsListWithEmptyListPlaceholder
import org.junit.Rule
import org.junit.Test
internal class MyLocationListTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun `show placeholder when no locations is added`() = runTest {
val myLocationsRobot = MyLocationListRobot(composeRule)
composeRule.setContentWrappedInTheme {
val noLocations = emptyList<Location>()
val myLocationsViewModel = FakeMyLocationsViewModel(locations = noLocations)
MyLocationsListWithEmptyListPlaceholder(
modifier = Modifier.fillMaxWidth(),
myLocationsViewModelApi = myLocationsViewModel
)
}
myLocationsRobot
.verifyNoLocationsPlaceHolderVisible()
}
private class FakeMyLocationsViewModel(
locations: List<Location> = emptyList()
) : MyLocationsViewModelApi {
private val locationsFlow = MutableStateFlow(locations.toMutableList())
private val selectedItemIndex = MutableStateFlow(if (locations.isNotEmpty()) 0 else -1)
private val _myLocations = locationsFlow
.combine(selectedItemIndex) { locations, selectedIndex ->
locations.mapIndexed { index, location ->
SelectableLocation(location, index == selectedIndex)
}
}
override val myLocationsFlow: Flow<List<SelectableLocation>> = _myLocations
override fun onAddLocation(locationToAdd: Location) {
val currentLocations = locationsFlow.value
currentLocations.add(locationToAdd)
locationsFlow.value = currentLocations
}
override fun onDeleteLocation(locationToDelete: Location) {
val currentLocations = locationsFlow.value
currentLocations.remove(locationToDelete)
locationsFlow.value = currentLocations
}
override fun onLocationSelected(selectedLocationIndex: Int) {
selectedItemIndex.value = selectedLocationIndex
}
}
private fun ComposeContentTestRule.setContentWrappedInTheme(content: @Composable () -> Unit) {
setContent {
IntUiTheme {
content()
}
}
}
}
private class MyLocationListRobot(private val composableRule: ComposeContentTestRule) {
fun clickOnItemWithText(text: String) {
composableRule
.onNodeWithText(text)
.performClick()
}
fun verifyNoLocationsPlaceHolderVisible() {
composableRule
.onNodeWithText("No locations added yet. Go and add the first location.")
.assertExists()
composableRule
.onNodeWithContentDescription("Empty list icon.")
.assertExists()
}
}