Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f00d50e
Updating library
adalpari Oct 1, 2025
dc07310
Porting the terms fetching
adalpari Oct 1, 2025
9358243
Porting create
adalpari Oct 1, 2025
9bb9e35
Porting delete term
adalpari Oct 1, 2025
5e62ed7
porting update term
adalpari Oct 1, 2025
726a4d3
Update term fix
adalpari Oct 1, 2025
3a0639b
Creating the new isHierarchical cocal field
adalpari Oct 1, 2025
6bce343
Parent fix
adalpari Oct 1, 2025
c39d8fb
Fixing tests
adalpari Oct 1, 2025
630a7b1
detekt
adalpari Oct 1, 2025
10b8ab2
Minor fix
adalpari Oct 1, 2025
03b8a91
Fixing tests
adalpari Oct 1, 2025
7347ee2
Adding the taxonomies menu view model
adalpari Oct 2, 2025
1a3a864
Adding show mechanism
adalpari Oct 2, 2025
025c3ea
Showing taxoniomies
adalpari Oct 2, 2025
a7de2e3
Adding a LiveData
adalpari Oct 2, 2025
d37d8a2
Call categories and tags screens
adalpari Oct 2, 2025
6f2bcd1
detekt and style
adalpari Oct 2, 2025
bbdc79f
Adding tests
adalpari Oct 3, 2025
8ec9ecc
Creating dataview
adalpari Oct 3, 2025
ba3efae
Style adjustments
adalpari Oct 3, 2025
b3e623b
Merge branch 'trunk' into task/cmm-814-hide-or-show-taxonomies-in-the…
adalpari Oct 3, 2025
417152b
Adjustments and setHierarchical
adalpari Oct 6, 2025
a249c0a
Proper indentation
adalpari Oct 6, 2025
543d686
Handling parent and description in details
adalpari Oct 6, 2025
d4386c9
Sorting hierarchically
adalpari Oct 6, 2025
cd96e04
Adding tests
adalpari Oct 6, 2025
c715f55
Title fix
adalpari Oct 6, 2025
e4d10fe
Removing unused string
adalpari Oct 6, 2025
13f26fc
Using server query
adalpari Oct 6, 2025
75d074d
Using server sorting
adalpari Oct 6, 2025
a6fe5ce
Merge branch 'trunk' into task/cmm-814-hide-or-show-taxonomies-in-the…
adalpari Oct 7, 2025
69ce363
Merge remote-tracking branch 'origin/task/cmm-814-hide-or-show-taxono…
adalpari Oct 7, 2025
da89442
Merge remote-tracking branch 'origin/trunk' into feature/CMM-802-Crea…
adalpari Oct 7, 2025
d2e9f03
Handling indentation in the UI style layer
adalpari Oct 7, 2025
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
4 changes: 4 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@
android:name=".ui.accounts.applicationpassword.ApplicationPasswordsListActivity"
android:label="@string/application_password_info_title"
android:theme="@style/WordPress.NoActionBar" />
<activity
android:name=".ui.taxonomies.TermsDataViewActivity"
android:label="@string/taxonomies_title"
android:theme="@style/WordPress.NoActionBar" />
<activity
android:name=".ui.prefs.notifications.NotificationsSettingsActivity"
android:configChanges="orientation|screenSize"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
import org.wordpress.android.ui.subscribers.SubscribersActivity;
import org.wordpress.android.ui.suggestion.SuggestionActivity;
import org.wordpress.android.ui.suggestion.SuggestionType;
import org.wordpress.android.ui.taxonomies.TermsDataViewActivity;
import org.wordpress.android.ui.themes.ThemeBrowserActivity;
import org.wordpress.android.ui.utils.PreMigrationDeepLinkData;
import org.wordpress.android.util.AppLog;
Expand Down Expand Up @@ -1899,4 +1900,10 @@ public static void openShareIntent(@NonNull Context context, @NonNull String lin
intent.putExtra(Intent.EXTRA_TITLE, title);
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_link)));
}

public static void showTermsList(@NonNull Context context, @NonNull String taxonomySlug,
@NonNull String taxonomyName, boolean isHierarchical) {
Intent intent = TermsDataViewActivity.Companion.getIntent(context, taxonomySlug, taxonomyName, isHierarchical);
context.startActivity(intent);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.wordpress.android.ui.dataview

import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/**
* Represents a basic model for data to be displayed in a [DataViewItemCard]. Note that [data] is
* optional but is intended to store the actual data object associated with the item..
Expand All @@ -11,5 +14,6 @@ data class DataViewItem(
val fields: List<DataViewItemField>,
// Avoid adding the last field to the end of the card and follow regular alignment instead
val skipEndPositioning: Boolean = false,
val data: Any? = null
val data: Any? = null,
val indentation: Dp = 0.dp // Used to indent items
)
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fun DataViewItemCard(
) {
Row(
modifier = Modifier
.padding(16.dp)
.padding(start = 16.dp + item.indentation, end = 16.dp, top = 16.dp , bottom = 16.dp)
.fillMaxWidth(),
) {
item.image?.let { image ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1143,14 +1143,8 @@ private void showTaxonomies(List<TaxonomyTypeDetailsWithEditContext> taxonomies)
pref.setTitle(taxonomy.getName());
pref.setKey(taxonomy.getSlug());
pref.setOnPreferenceClickListener(preference -> {
// TODO: Create generic taxonomies DataView and call it from here
// We are not accepting the taxonomy name as a parameter yet
// So Categories and Tags are still hardcoded
if ("category".equals(taxonomy.getSlug())) {
ActivityLauncher.showCategoriesList(getActivity(), mSite);
} else if ("post_tag".equals(taxonomy.getSlug())) {
SiteSettingsTagListActivity.showTagList(getActivity(), mSite);
}
ActivityLauncher.showTermsList(getActivity(), taxonomy.getSlug(), taxonomy.getName(),
taxonomy.getHierarchical());
return false;
}
);
Expand Down
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
Copy link
Member

Choose a reason for hiding this comment

The 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 TermDetailCard? I.e., perform the filtering higher in the Compose tree.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 DataView flow does not work that way, so a refactor is needed to do that.

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)
}
}
}
Loading