diff --git a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt index 0855144..181a4ab 100644 --- a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt +++ b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt @@ -7,7 +7,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color 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.unit.dp 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.theme.JewelTheme 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.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.services.LocationsProvider import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi @@ -76,14 +81,58 @@ private fun LeftColumn( Spacer(modifier = Modifier.height(10.dp)) - MyLocationsList(Modifier.fillMaxSize(), myLocationsViewModelApi) + MyLocationsListWithEmptyListPlaceholder(Modifier.fillMaxSize(), myLocationsViewModelApi) } } @Composable -internal fun MyLocationsList(modifier: Modifier = Modifier, myLocationsViewModelApi: MyLocationsViewModelApi) { +internal fun MyLocationsListWithEmptyListPlaceholder( + modifier: Modifier = Modifier, + myLocationsViewModelApi: MyLocationsViewModelApi +) { 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, + modifier: Modifier, + myLocationsViewModelApi: MyLocationsViewModelApi +) { val listState = rememberSelectableLazyListState() // JEWEL-938 This will trigger on SelectableLazyColum's `onSelectedIndexesChange` callback LaunchedEffect(myLocations) { diff --git a/src/main/resources/messages/ComposeTemplate.properties b/src/main/resources/messages/ComposeTemplate.properties index becc060..39ddfb4 100644 --- a/src/main/resources/messages/ComposeTemplate.properties +++ b/src/main/resources/messages/ComposeTemplate.properties @@ -2,6 +2,8 @@ weather.app.temperature.text={0}\u00B0C weather.app.humidity.text=Humidity: {0}% weather.app.wind.direction.text=Wind: {0} km/h {1} 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.content.description=Add a place to a watch list. weather.app.7days.forecast.title.text=7-day Forecast \ No newline at end of file diff --git a/src/test/kotlin/org/jetbrains/plugins/template/MyLocationListTest.kt b/src/test/kotlin/org/jetbrains/plugins/template/MyLocationListTest.kt new file mode 100644 index 0000000..84d5491 --- /dev/null +++ b/src/test/kotlin/org/jetbrains/plugins/template/MyLocationListTest.kt @@ -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() + val myLocationsViewModel = FakeMyLocationsViewModel(locations = noLocations) + MyLocationsListWithEmptyListPlaceholder( + modifier = Modifier.fillMaxWidth(), + myLocationsViewModelApi = myLocationsViewModel + ) + } + + myLocationsRobot + .verifyNoLocationsPlaceHolderVisible() + } + + private class FakeMyLocationsViewModel( + locations: List = 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> = _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() + } +} \ No newline at end of file