diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml
index 9f5767a0da1e..624aac556cb6 100644
--- a/WordPress/src/main/AndroidManifest.xml
+++ b/WordPress/src/main/AndroidManifest.xml
@@ -252,6 +252,10 @@
android:name=".ui.accounts.applicationpassword.ApplicationPasswordsListActivity"
android:label="@string/application_password_info_title"
android:theme="@style/WordPress.NoActionBar" />
+
,
// 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
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt
index b09012025ce8..c2c72a63cac2 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/dataview/DataViewItemCard.kt
@@ -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 ->
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
index 4993b9669db5..9f54e9516cfc 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
@@ -1143,14 +1143,8 @@ private void showTaxonomies(List 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;
}
);
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/taxonomies/TermsDataViewActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/taxonomies/TermsDataViewActivity.kt
new file mode 100644
index 000000000000..399d55270269
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/taxonomies/TermsDataViewActivity.kt
@@ -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()
+
+ 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(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,
+ 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, 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
+ 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)
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/taxonomies/TermsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/taxonomies/TermsViewModel.kt
new file mode 100644
index 000000000000..05476bf5d764
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/taxonomies/TermsViewModel.kt
@@ -0,0 +1,254 @@
+package org.wordpress.android.ui.taxonomies
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.compose.ui.unit.dp
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import org.wordpress.android.R
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
+import org.wordpress.android.fluxc.store.AccountStore
+import org.wordpress.android.fluxc.store.TaxonomyStore.DEFAULT_TAXONOMY_CATEGORY
+import org.wordpress.android.fluxc.store.TaxonomyStore.DEFAULT_TAXONOMY_TAG
+import org.wordpress.android.fluxc.utils.AppLogWrapper
+import org.wordpress.android.modules.IO_THREAD
+import org.wordpress.android.modules.UI_THREAD
+import org.wordpress.android.ui.dataview.DataViewDropdownItem
+import org.wordpress.android.ui.dataview.DataViewFieldType
+import org.wordpress.android.ui.dataview.DataViewItem
+import org.wordpress.android.ui.dataview.DataViewItemField
+import org.wordpress.android.ui.dataview.DataViewViewModel
+import org.wordpress.android.ui.mysite.SelectedSiteRepository
+import org.wordpress.android.util.AppLog
+import org.wordpress.android.util.NetworkUtilsWrapper
+import rs.wordpress.api.kotlin.WpRequestResult
+import uniffi.wp_api.TermEndpointType
+import uniffi.wp_api.TermListParams
+import uniffi.wp_api.AnyTermWithEditContext
+import uniffi.wp_api.WpApiParamOrder
+import uniffi.wp_api.WpApiParamTermsOrderBy
+import javax.inject.Inject
+import javax.inject.Named
+
+private const val INDENTATION_IN_DP = 10
+
+@HiltViewModel
+class TermsViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val wpApiClientProvider: WpApiClientProvider,
+ private val appLogWrapper: AppLogWrapper,
+ private val selectedSiteRepository: SelectedSiteRepository,
+ accountStore: AccountStore,
+ @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher,
+ sharedPrefs: SharedPreferences,
+ networkUtilsWrapper: NetworkUtilsWrapper,
+ @Named(IO_THREAD) ioDispatcher: CoroutineDispatcher,
+) : DataViewViewModel(
+ mainDispatcher = mainDispatcher,
+ appLogWrapper = appLogWrapper,
+ sharedPrefs = sharedPrefs,
+ networkUtilsWrapper = networkUtilsWrapper,
+ selectedSiteRepository = selectedSiteRepository,
+ accountStore = accountStore,
+ ioDispatcher = ioDispatcher
+) {
+ private var taxonomySlug: String = ""
+ private var isHierarchical: Boolean = false
+ private var currentTerms = listOf()
+
+ fun initialize(taxonomySlug: String, isHierarchical: Boolean) {
+ this.taxonomySlug = taxonomySlug
+ this.isHierarchical = isHierarchical
+ initialize()
+ }
+
+ override fun getSupportedSorts(): List = if (isHierarchical) {
+ listOf()
+ } else {
+ listOf(
+ DataViewDropdownItem(id = SORT_BY_NAME_ID, titleRes = R.string.term_sort_by_name),
+ DataViewDropdownItem(id = SORT_BY_COUNT_ID, titleRes = R.string.term_sort_by_count),
+ )
+ }
+
+ override suspend fun performNetworkRequest(
+ page: Int,
+ searchQuery: String,
+ filter: DataViewDropdownItem?,
+ sortOrder: WpApiParamOrder,
+ sortBy: DataViewDropdownItem?,
+ ): List = withContext(ioDispatcher) {
+ val selectedSite = selectedSiteRepository.getSelectedSite()
+
+ if (selectedSite == null) {
+ val error = "No selected site to get Terms"
+ appLogWrapper.e(AppLog.T.API, error)
+ onError(error)
+ return@withContext emptyList()
+ }
+
+ val allTerms = getTermsList(selectedSite, page, searchQuery, sortOrder, sortBy)
+ currentTerms = allTerms
+
+ // Sort the results hierarchically if necessary
+ val sortedTerms = if (isHierarchical) {
+ sortByHierarchy(terms = allTerms)
+ } else {
+ allTerms
+ }
+
+ // Convert to DataViewItems and return
+ sortedTerms.map { term ->
+ // Do not use hierarchical indentation when the user is searching terms
+ convertToDataViewItem(allTerms, term, isHierarchical && searchQuery.isEmpty())
+ }
+ }
+
+ private fun sortByHierarchy(terms: List): List {
+ val result = mutableListOf()
+ val termsById = terms.associateBy { it.id }
+ val visited = mutableSetOf()
+
+ fun addTermWithChildren(term: AnyTermWithEditContext) {
+ if (term.id in visited) return
+ visited.add(term.id)
+ result.add(term)
+
+ // Find and add all direct children
+ terms.filter { it.parent == term.id }
+ .sortedBy { it.name }
+ .forEach { child ->
+ addTermWithChildren(child)
+ }
+ }
+
+ // First, add all root terms (those with parent == 0 or no parent in the list)
+ terms.filter { it.parent == 0L || termsById[it.parent] == null }
+ .sortedBy { it.name }
+ .forEach { rootTerm ->
+ addTermWithChildren(rootTerm)
+ }
+
+ return result
+ }
+
+ fun getTerm(termId: Long): AnyTermWithEditContext? {
+ val item = uiState.value.items.firstOrNull {
+ (it.data as? AnyTermWithEditContext)?.id == termId
+ }
+ return item?.data as? AnyTermWithEditContext
+ }
+
+ fun getAllTerms(): List = currentTerms
+
+ private fun convertToDataViewItem(
+ allTerms: List,
+ term: AnyTermWithEditContext,
+ useHierarchicalIndentation: Boolean
+ ): DataViewItem {
+ val indentation = if (useHierarchicalIndentation) {
+ getHierarchicalIndentation(allTerms, term)
+ } else {
+ 0
+ }
+ return DataViewItem(
+ id = term.id,
+ image = null,
+ title = term.name,
+ fields = listOf(
+ DataViewItemField(
+ value = context.resources.getString(R.string.term_count, term.count),
+ valueType = DataViewFieldType.TEXT,
+ )
+ ),
+ skipEndPositioning = true,
+ data = term,
+ indentation = (indentation * INDENTATION_IN_DP).dp
+ )
+ }
+
+
+ /**
+ * Returns an integer representation of the hierarchical indentation for the given term.
+ */
+ private fun getHierarchicalIndentation(
+ allTerms: List,
+ term: AnyTermWithEditContext?
+ ): Int {
+ if (term == null) return 0
+
+ val termsById = allTerms.associateBy { it.id }
+ var indentation = 0
+ var currentParentId = term.parent
+
+ while (currentParentId != null && currentParentId > 0) {
+ val parent = termsById[currentParentId]
+ if (parent == null) break
+ indentation++
+ currentParentId = parent.parent
+ }
+
+ return indentation
+ }
+
+ private suspend fun getTermsList(
+ site: SiteModel,
+ page: Int,
+ searchQuery: String,
+ sortOrder: WpApiParamOrder,
+ sortBy: DataViewDropdownItem?
+ ): List {
+ val wpApiClient = wpApiClientProvider.getWpApiClient(site)
+
+ val termEndpointType = when (taxonomySlug) {
+ DEFAULT_TAXONOMY_CATEGORY -> TermEndpointType.Categories
+ DEFAULT_TAXONOMY_TAG -> TermEndpointType.Tags
+ else -> TermEndpointType.Custom(taxonomySlug)
+ }
+
+ val termsResponse = wpApiClient.request { requestBuilder ->
+ requestBuilder.terms().listWithEditContext(
+ termEndpointType = termEndpointType,
+ params = TermListParams(
+ page = page.toUInt(),
+ search = searchQuery,
+ order = when (sortOrder) {
+ WpApiParamOrder.ASC -> WpApiParamOrder.ASC
+ WpApiParamOrder.DESC -> WpApiParamOrder.DESC
+ },
+ orderby = if (sortBy == null) {
+ null
+ } else {
+ if (sortBy.id == SORT_BY_COUNT_ID) {
+ WpApiParamTermsOrderBy.COUNT
+ } else {
+ WpApiParamTermsOrderBy.NAME // default
+ }
+ }
+ )
+ )
+ }
+
+ return when (termsResponse) {
+ is WpRequestResult.Success -> {
+ appLogWrapper.d(AppLog.T.API, "Fetched ${termsResponse.response.data.size} terms")
+ termsResponse.response.data
+ }
+
+ else -> {
+ val error = "Error getting Terms list for taxonomy: $taxonomySlug"
+ appLogWrapper.e(AppLog.T.API, error)
+ onError(error)
+ emptyList()
+ }
+ }
+ }
+
+ companion object {
+ private const val SORT_BY_NAME_ID = 1L
+ private const val SORT_BY_COUNT_ID = 2L
+ }
+}
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index 2814e693a892..18a22ddc45ee 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -5086,4 +5086,14 @@ translators: %s: Select control option value e.g: "Auto, 25%". -->
There\'s nothing here
+
+
+ Name:
+ Slug:
+ Description:
+ Count:
+ Count: %1$d
+ Parent:
+ Name
+ Count
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/taxonomies/TermsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/taxonomies/TermsViewModelTest.kt
new file mode 100644
index 000000000000..3fcebaa07a85
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/taxonomies/TermsViewModelTest.kt
@@ -0,0 +1,96 @@
+package org.wordpress.android.ui.taxonomies
+
+import android.content.Context
+import android.content.SharedPreferences
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.R
+import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
+import org.wordpress.android.fluxc.store.AccountStore
+import org.wordpress.android.fluxc.store.TaxonomyStore.DEFAULT_TAXONOMY_CATEGORY
+import org.wordpress.android.fluxc.store.TaxonomyStore.DEFAULT_TAXONOMY_TAG
+import org.wordpress.android.fluxc.utils.AppLogWrapper
+import org.wordpress.android.ui.mysite.SelectedSiteRepository
+import org.wordpress.android.util.NetworkUtilsWrapper
+
+@ExperimentalCoroutinesApi
+class TermsViewModelTest : BaseUnitTest() {
+ @Mock
+ private lateinit var context: Context
+
+ @Mock
+ private lateinit var wpApiClientProvider: WpApiClientProvider
+
+ @Mock
+ private lateinit var appLogWrapper: AppLogWrapper
+
+ @Mock
+ private lateinit var selectedSiteRepository: SelectedSiteRepository
+
+ @Mock
+ private lateinit var accountStore: AccountStore
+
+ @Mock
+ private lateinit var sharedPrefs: SharedPreferences
+
+ @Mock
+ private lateinit var networkUtilsWrapper: NetworkUtilsWrapper
+
+ @Before
+ fun setUp() {
+ // Minimal setup - add more mocks in individual tests as needed
+ }
+
+ private fun createViewModel(): TermsViewModel {
+ return TermsViewModel(
+ context = context,
+ wpApiClientProvider = wpApiClientProvider,
+ appLogWrapper = appLogWrapper,
+ selectedSiteRepository = selectedSiteRepository,
+ accountStore = accountStore,
+ mainDispatcher = testDispatcher(),
+ sharedPrefs = sharedPrefs,
+ networkUtilsWrapper = networkUtilsWrapper,
+ ioDispatcher = testDispatcher()
+ )
+ }
+
+ @Test
+ fun `getSupportedSorts returns empty list for hierarchical taxonomies`() {
+ val viewModel = createViewModel()
+ viewModel.initialize(DEFAULT_TAXONOMY_CATEGORY, isHierarchical = true)
+
+ val supportedSorts = viewModel.getSupportedSorts()
+
+ assertThat(supportedSorts).isEmpty()
+ }
+
+ @Test
+ fun `getSupportedSorts returns sort options for non-hierarchical taxonomies`() {
+ val viewModel = createViewModel()
+ viewModel.initialize(DEFAULT_TAXONOMY_TAG, isHierarchical = false)
+
+ val supportedSorts = viewModel.getSupportedSorts()
+
+ assertThat(supportedSorts).hasSize(2)
+ assertThat(supportedSorts[0].titleRes).isEqualTo(R.string.term_sort_by_name)
+ assertThat(supportedSorts[1].titleRes).isEqualTo(R.string.term_sort_by_count)
+ }
+
+ @Test
+ fun `network unavailable sets offline state`() = test {
+ whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
+
+ val viewModel = createViewModel()
+ viewModel.initialize(DEFAULT_TAXONOMY_CATEGORY, isHierarchical = true)
+ advanceUntilIdle()
+
+ assertThat(viewModel.uiState.value.loadingState)
+ .isEqualTo(org.wordpress.android.ui.dataview.LoadingState.OFFLINE)
+ }
+}