-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CMM-802 create taxonomies data view #22258
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
Changes from all commits
f00d50e
dc07310
9358243
9bb9e35
5e62ed7
726a4d3
3a0639b
6bce343
c39d8fb
630a7b1
10b8ab2
03b8a91
7347ee2
1a3a864
025c3ea
a7de2e3
d37d8a2
6f2bcd1
bbdc79f
8ec9ecc
ba3efae
b3e623b
417152b
a249c0a
543d686
d4386c9
cd96e04
c715f55
e4d10fe
13f26fc
75d074d
a6fe5ce
69ce363
da89442
d2e9f03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
package org.wordpress.android.ui.taxonomies | ||
|
||
import android.content.Context | ||
import android.content.Intent | ||
import android.os.Build | ||
import android.os.Bundle | ||
import androidx.activity.viewModels | ||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Row | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.rememberScrollState | ||
import androidx.compose.foundation.verticalScroll | ||
import androidx.compose.material.icons.Icons | ||
import androidx.compose.material.icons.automirrored.filled.ArrowBack | ||
import androidx.compose.material3.Card | ||
import androidx.compose.material3.CardDefaults | ||
import androidx.compose.material3.ExperimentalMaterial3Api | ||
import androidx.compose.material3.Icon | ||
import androidx.compose.material3.IconButton | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.material3.Text | ||
import androidx.compose.material3.TopAppBar | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.collectAsState | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.ComposeView | ||
import androidx.compose.ui.platform.ViewCompositionStrategy | ||
import androidx.compose.ui.res.stringResource | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.unit.dp | ||
import androidx.navigation.NavHostController | ||
import androidx.navigation.compose.NavHost | ||
import androidx.navigation.compose.composable | ||
import androidx.navigation.compose.rememberNavController | ||
import dagger.hilt.android.AndroidEntryPoint | ||
import org.wordpress.android.R | ||
import org.wordpress.android.fluxc.utils.AppLogWrapper | ||
import org.wordpress.android.ui.compose.theme.AppThemeM3 | ||
import org.wordpress.android.ui.dataview.DataViewScreen | ||
import org.wordpress.android.ui.main.BaseAppCompatActivity | ||
import org.wordpress.android.util.AppLog | ||
import uniffi.wp_api.AnyTermWithEditContext | ||
import javax.inject.Inject | ||
|
||
@AndroidEntryPoint | ||
class TermsDataViewActivity : BaseAppCompatActivity() { | ||
@Inject | ||
lateinit var appLogWrapper: AppLogWrapper | ||
|
||
private val viewModel by viewModels<TermsViewModel>() | ||
|
||
private lateinit var composeView: ComposeView | ||
private lateinit var navController: NavHostController | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
val taxonomySlug = intent.getStringExtra(TAXONOMY_SLUG) | ||
val isHierarchical = intent.getBooleanExtra(IS_HIERARCHICAL, false) | ||
val taxonomyName = intent.getStringExtra(TAXONOMY_NAME) ?: "" | ||
if (taxonomySlug == null) { | ||
appLogWrapper.e(AppLog.T.API, "Error: No taxonomy selected") | ||
finish() | ||
return | ||
} | ||
|
||
viewModel.initialize(taxonomySlug, isHierarchical) | ||
|
||
composeView = ComposeView(this) | ||
setContentView( | ||
composeView.apply { | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
this.isForceDarkAllowed = false | ||
} | ||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||
setContent { | ||
NavigableContent(taxonomyName) | ||
} | ||
} | ||
) | ||
} | ||
|
||
private enum class TermScreen { | ||
List, | ||
Detail | ||
} | ||
|
||
@OptIn(ExperimentalMaterial3Api::class) | ||
@Composable | ||
private fun NavigableContent(taxonomyName: String) { | ||
navController = rememberNavController() | ||
val listTitle = taxonomyName | ||
val titleState = remember { mutableStateOf(listTitle) } | ||
|
||
AppThemeM3 { | ||
Scaffold( | ||
topBar = { | ||
TopAppBar( | ||
title = { Text(titleState.value) }, | ||
navigationIcon = { | ||
IconButton(onClick = { | ||
if (navController.previousBackStackEntry != null) { | ||
navController.navigateUp() | ||
} else { | ||
finish() | ||
} | ||
}) { | ||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) | ||
} | ||
} | ||
) | ||
}, | ||
) { contentPadding -> | ||
NavHost( | ||
navController = navController, | ||
startDestination = TermScreen.List.name | ||
) { | ||
composable(route = TermScreen.List.name) { | ||
titleState.value = listTitle | ||
ShowListScreen( | ||
navController, | ||
modifier = Modifier.padding(contentPadding) | ||
) | ||
} | ||
|
||
composable(route = TermScreen.Detail.name) { | ||
navController.previousBackStackEntry?.savedStateHandle?.let { handle -> | ||
val termId = handle.get<Long>(KEY_TERM_ID) | ||
if (termId != null) { | ||
viewModel.getTerm(termId)?.let { term -> | ||
titleState.value = term.name | ||
ShowTermDetailScreen( | ||
allTerms = viewModel.getAllTerms(), | ||
term = term, | ||
modifier = Modifier.padding(contentPadding) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun ShowListScreen( | ||
navController: NavHostController, | ||
modifier: Modifier | ||
) { | ||
DataViewScreen( | ||
uiState = viewModel.uiState.collectAsState(), | ||
supportedFilters = viewModel.getSupportedFilters(), | ||
supportedSorts = viewModel.getSupportedSorts(), | ||
onRefresh = { | ||
viewModel.onRefreshData() | ||
}, | ||
onFetchMore = { | ||
viewModel.onFetchMoreData() | ||
}, | ||
onSearchQueryChange = { query -> | ||
viewModel.onSearchQueryChange(query) | ||
}, | ||
onItemClick = { item -> | ||
viewModel.onItemClick(item) | ||
(item.data as? AnyTermWithEditContext)?.let { term -> | ||
navController.currentBackStackEntry?.savedStateHandle?.set( | ||
key = KEY_TERM_ID, | ||
value = term.id | ||
) | ||
navController.navigate(route = TermScreen.Detail.name) | ||
} | ||
}, | ||
onFilterClick = { filter -> | ||
viewModel.onFilterClick(filter) | ||
}, | ||
onSortClick = { sort -> | ||
viewModel.onSortClick(sort) | ||
}, | ||
onSortOrderClick = { order -> | ||
viewModel.onSortOrderClick(order) | ||
}, | ||
emptyView = viewModel.emptyView, | ||
modifier = modifier | ||
) | ||
} | ||
|
||
@Composable | ||
private fun ShowTermDetailScreen( | ||
allTerms: List<AnyTermWithEditContext>, | ||
term: AnyTermWithEditContext, | ||
modifier: Modifier | ||
) { | ||
Column( | ||
modifier = modifier | ||
.fillMaxSize() | ||
.padding(16.dp) | ||
.verticalScroll(rememberScrollState()), | ||
verticalArrangement = Arrangement.spacedBy(16.dp) | ||
) { | ||
TermDetailsCard(allTerms, term) | ||
} | ||
} | ||
|
||
@Composable | ||
private fun TermDetailsCard(allTerms: List<AnyTermWithEditContext>, term: AnyTermWithEditContext) { | ||
Card( | ||
modifier = Modifier.fillMaxWidth(), | ||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), | ||
colors = CardDefaults.cardColors( | ||
containerColor = MaterialTheme.colorScheme.surface | ||
) | ||
) { | ||
Column( | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.padding(16.dp), | ||
verticalArrangement = Arrangement.spacedBy(12.dp) | ||
) { | ||
DetailRow( | ||
label = stringResource(R.string.term_name_label), | ||
value = term.name | ||
) | ||
|
||
DetailRow( | ||
label = stringResource(R.string.term_slug_label), | ||
value = term.slug | ||
) | ||
|
||
DetailRow( | ||
label = stringResource(R.string.term_description_label), | ||
value = term.description | ||
) | ||
|
||
DetailRow( | ||
label = stringResource(R.string.term_count_label), | ||
value = term.count.toString() | ||
) | ||
|
||
term.parent?.let { parentId -> | ||
val parentName = allTerms.firstOrNull { it.id == parentId }?.name | ||
adalpari marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asking for my own education to better understand Jetpack Compose: are there downsides (performance?) to passing the entire list of items for filtering the parent name? Should we instead provide the parent or its name to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there is one downside: running the filtering in the UI thread. But no matter at what level it is filtered, I don't see it making any difference. We are executing it in every CardDetails composition. I don't like this approach tbh, because the Composable items are asking directly to the ViewModel (and even performing some logic). I would prefer the ViewModel executing all the logic and giving just the state to the View. But the current But from this discussion, I think I'll give it a try. So, thank you :) |
||
parentName?.let { | ||
DetailRow( | ||
label = stringResource(R.string.term_parent_label), | ||
value = parentName | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun DetailRow( | ||
label: String, | ||
value: String | ||
) { | ||
Row( | ||
modifier = Modifier.fillMaxWidth(), | ||
verticalAlignment = Alignment.Top | ||
) { | ||
Text( | ||
text = label, | ||
style = MaterialTheme.typography.bodyMedium, | ||
fontWeight = FontWeight.SemiBold, | ||
color = MaterialTheme.colorScheme.onSurfaceVariant, | ||
modifier = Modifier.weight(0.3f) | ||
) | ||
|
||
Text( | ||
text = value, | ||
style = MaterialTheme.typography.bodyMedium, | ||
color = MaterialTheme.colorScheme.onSurface, | ||
modifier = Modifier.weight(0.7f) | ||
) | ||
} | ||
} | ||
|
||
companion object { | ||
private const val TAXONOMY_SLUG = "taxonomy_slug" | ||
private const val IS_HIERARCHICAL = "is_hierarchical" | ||
private const val TAXONOMY_NAME = "taxonomy_name" | ||
private const val KEY_TERM_ID = "termId" | ||
|
||
fun getIntent(context: Context, taxonomySlug: String, taxonomyName: String, isHierarchical: Boolean): Intent = | ||
Intent(context, TermsDataViewActivity::class.java).apply { | ||
putExtra(TAXONOMY_SLUG, taxonomySlug) | ||
putExtra(TAXONOMY_NAME, taxonomyName) | ||
putExtra(IS_HIERARCHICAL, isHierarchical) | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.