diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index 66dc66748712..5a2f11a65562 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -49,6 +49,7 @@ import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel; import org.wordpress.android.ui.prefs.homepage.HomepageSettingsViewModel; import org.wordpress.android.ui.prefs.language.LocalePickerViewModel; +import org.wordpress.android.ui.prefs.taxonomies.TaxonomiesNavMenuViewModel; import org.wordpress.android.ui.prefs.timezone.SiteSettingsTimezoneViewModel; import org.wordpress.android.ui.publicize.PublicizeListViewModel; import org.wordpress.android.ui.reader.ReaderCommentListViewModel; @@ -495,6 +496,12 @@ abstract class ViewModelModule { @ViewModelKey(BloggingRemindersViewModel.class) abstract ViewModel bloggingRemindersViewModel(BloggingRemindersViewModel viewModel); + @Binds + @IntoMap + @ViewModelKey(TaxonomiesNavMenuViewModel.class) + abstract ViewModel taxonomiesNavMenuViewModel(TaxonomiesNavMenuViewModel viewModel); + + @Binds @IntoMap @ViewModelKey(LocalePickerViewModel.class) 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 2d5e32b26262..4993b9669db5 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 @@ -84,6 +84,7 @@ import org.wordpress.android.ui.prefs.EditTextPreferenceWithValidation.ValidationType; import org.wordpress.android.ui.prefs.SiteSettingsFormatDialog.FormatType; import org.wordpress.android.ui.prefs.homepage.HomepageSettingsDialog; +import org.wordpress.android.ui.prefs.taxonomies.TaxonomiesNavMenuViewModel; import org.wordpress.android.ui.prefs.timezone.SiteSettingsTimezoneBottomSheet; import org.wordpress.android.ui.utils.UiHelpers; import org.wordpress.android.util.AppLog; @@ -114,6 +115,7 @@ import javax.inject.Inject; import kotlin.Triple; +import uniffi.wp_api.TaxonomyTypeDetailsWithEditContext; import static org.wordpress.android.ui.prefs.WPComSiteSettings.supportsJetpackSiteAcceleratorSettings; @@ -194,6 +196,8 @@ public class SiteSettingsFragment extends PreferenceFragment private BloggingRemindersViewModel mBloggingRemindersViewModel; + private TaxonomiesNavMenuViewModel mTaxonomiesNavMenuViewModel; + public SiteModel mSite; // Can interface with WP.com or WP.org @@ -1107,6 +1111,52 @@ public void initPreferences() { initBloggingSection(); removeEmptyCategories(); + initTaxonomies(); + } + + private void initTaxonomies() { + mTaxonomiesNavMenuViewModel = new ViewModelProvider(getAppCompatActivity(), mViewModelFactory) + .get(TaxonomiesNavMenuViewModel.class); + mTaxonomiesNavMenuViewModel.getTaxonomies().observe(getAppCompatActivity(), this::showTaxonomies); + mTaxonomiesNavMenuViewModel.fetchTaxonomies(mSite); + } + + private void showTaxonomies(List taxonomies) { + if (taxonomies.isEmpty()) { + return; + } + PreferenceGroup siteScreen = (PreferenceGroup) findPreference(getString(R.string.pref_key_site_screen)); + if (siteScreen != null) { + // Create taxonomies preference group + final String taxonomiesPrefKey = getString(R.string.pref_key_taxonomies); + PreferenceGroup taxonomiesPreference = (PreferenceGroup) findPreference(taxonomiesPrefKey); + if (taxonomiesPreference != null) { + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_taxonomies); + } + taxonomiesPreference = new PreferenceCategory(getActivity()); + taxonomiesPreference.setTitle(getString(R.string.taxonomies_title)); + taxonomiesPreference.setKey(taxonomiesPrefKey); + siteScreen.addPreference(taxonomiesPreference); + + for (TaxonomyTypeDetailsWithEditContext taxonomy : taxonomies) { + Preference pref = new Preference(getActivity()); + 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); + } + return false; + } + ); + taxonomiesPreference.addPreference(pref); + } + } } private void updateHomepageSummary() { @@ -2033,18 +2083,7 @@ private void removeNonSelfHostedPreferences() { if (group != null) { group.removeAll(); } - if (mSite.isUsingSelfHostedRestApi()) { - // Remove everything inside "Writing" preference but "Categories" and "Tags" which are now supported - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_category); - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_format); - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_date_format); - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_time_format); - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_week_start); - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_posts_per_page); - WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts); - } else { - WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_writing); - } + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_writing); WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_discussion); WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_advanced); WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_quota); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/taxonomies/TaxonomiesNavMenuViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/taxonomies/TaxonomiesNavMenuViewModel.kt new file mode 100644 index 000000000000..1e01183fdac6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/taxonomies/TaxonomiesNavMenuViewModel.kt @@ -0,0 +1,58 @@ +package org.wordpress.android.ui.prefs.taxonomies + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.util.AppLog +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.TaxonomyListParams +import uniffi.wp_api.TaxonomyTypeDetailsWithEditContext +import javax.inject.Inject + +class TaxonomiesNavMenuViewModel @Inject constructor( + private val wpApiClientProvider: WpApiClientProvider, + private val appLogWrapper: AppLogWrapper, +) : ViewModel() { + // LiveData because this is observed from Java + private val _taxonomies = MutableLiveData>() + val taxonomies: LiveData> = _taxonomies + + fun fetchTaxonomies(site: SiteModel) { + if (!site.isUsingSelfHostedRestApi) { + appLogWrapper.d( + AppLog.T.API, + "Taxonomies - Taxonomies cannot be fetched: Application Password not available" + ) + return + } + viewModelScope.launch { + val client = wpApiClientProvider.getWpApiClient(site) + val response = client.request { requestBuilder -> + requestBuilder.taxonomies().listWithEditContext(TaxonomyListParams()) + } + when (response) { + is WpRequestResult.Success -> { + val list = response.response.data + appLogWrapper.d(AppLog.T.API, "Taxonomies - Fetched taxonomies ${list.taxonomyTypes.size}") + val taxonomies = mutableListOf() + list.taxonomyTypes.forEach { type -> + appLogWrapper.d(AppLog.T.API, "Taxonomies - Taxonomy ${type.value.name}") + if (type.value.visibility.showInNavMenus) { + taxonomies.add(type.value) + } + } + _taxonomies.value = taxonomies + } + + else -> { + appLogWrapper.e(AppLog.T.API, "Taxonomies - Error fetching taxonomies") + } + } + } + } +} diff --git a/WordPress/src/main/res/values/key_strings.xml b/WordPress/src/main/res/values/key_strings.xml index 3c3326c8cc4e..dde04cfdb29c 100644 --- a/WordPress/src/main/res/values/key_strings.xml +++ b/WordPress/src/main/res/values/key_strings.xml @@ -17,6 +17,7 @@ wp_pref_language wp_pref_app_theme wp_pref_whats_new + wp_pref_taxonomies wp_pref_notification_blogs pref_notification_blogs_followed wp_pref_notification_other_category diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index e50bd3029a3e..2814e693a892 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -590,6 +590,8 @@ Top level Permanently delete \'%s\' Category? + Taxonomies + Add to new post diff --git a/WordPress/src/test/java/org/wordpress/android/ui/prefs/taxonomies/TaxonomiesNavMenuViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/prefs/taxonomies/TaxonomiesNavMenuViewModelTest.kt new file mode 100644 index 000000000000..f87417e3a6bd --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/prefs/taxonomies/TaxonomiesNavMenuViewModelTest.kt @@ -0,0 +1,171 @@ +package org.wordpress.android.ui.prefs.taxonomies + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.util.AppLog +import rs.wordpress.api.kotlin.WpApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.TaxonomiesRequestListWithEditContextResponse +import uniffi.wp_api.TaxonomyType +import uniffi.wp_api.TaxonomyTypeDetailsWithEditContext +import uniffi.wp_api.TaxonomyTypesResponseWithEditContext +import uniffi.wp_api.TaxonomyTypeVisibility +import uniffi.wp_api.WpNetworkHeaderMap + +@ExperimentalCoroutinesApi +class TaxonomiesNavMenuViewModelTest : BaseUnitTest() { + @Mock + private lateinit var wpApiClientProvider: WpApiClientProvider + + @Mock + private lateinit var wpApiClient: WpApiClient + + @Mock + private lateinit var appLogWrapper: AppLogWrapper + + private lateinit var viewModel: TaxonomiesNavMenuViewModel + + private var taxonomies: List = listOf() + + private val testSite = SiteModel().apply { + id = 123 + url = "https://test.wordpress.com" + apiRestUsernamePlain = "user" + apiRestPasswordPlain = "pass" + setIsWPCom(false) + } + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + whenever(wpApiClientProvider.getWpApiClient(testSite)).thenReturn(wpApiClient) + + viewModel = TaxonomiesNavMenuViewModel( + wpApiClientProvider, + appLogWrapper + ) + viewModel.taxonomies.observeForever { taxonomies = it } + } + + @Test + fun `when site does not support self-hosted rest api, then taxonomies are not fetched`() = test { + testSite.setIsWPCom(true) + + viewModel.fetchTaxonomies(testSite) + advanceUntilIdle() + + verify(wpApiClientProvider, never()).getWpApiClient(any(), any()) + verify(appLogWrapper).d( + AppLog.T.API, + "Taxonomies - Taxonomies cannot be fetched: Application Password not available" + ) + assertTrue(taxonomies.isEmpty()) + } + + @Test + fun `when LiveData is observed, it starts with null value`() { + assertNotNull(viewModel.taxonomies) + assertEquals(null, viewModel.taxonomies.value) + } + + @Test + fun `fetch taxonomies with success response dispatches success action`() = runTest { + // Create the correct response structure following the MediaRSApiRestClientTest pattern + val response = TaxonomiesRequestListWithEditContextResponse( + createTestTaxonomyTypesResponseWithEditContext(), + mock(), + ) + + val successResponse: WpRequestResult = WpRequestResult.Success( + response = response + ) + + whenever(wpApiClient.request(any())).thenReturn(successResponse) + + viewModel.fetchTaxonomies(testSite) + advanceUntilIdle() + + val responseList: List = response.data.taxonomyTypes.map { it.value } + assertEquals(responseList, taxonomies) + } + + @Test + fun `fetch taxonomies with error response do nothing`() = runTest { + // Use a concrete error type that we can create - UnknownError requires statusCode and response + val errorResponse = WpRequestResult.UnknownError( + statusCode = 500u, + response = "Internal Server Error" + ) + + whenever(wpApiClient.request(any())).thenReturn(errorResponse) + + viewModel.fetchTaxonomies(testSite) + + verify(appLogWrapper).e(any(), any()) + assertTrue(taxonomies.isEmpty()) + } + + private fun createTestTaxonomyTypesResponseWithEditContext(): TaxonomyTypesResponseWithEditContext { + val visibility = TaxonomyTypeVisibility( + public = true, + publiclyQueryable = true, + showUi = true, + showAdminColumn = true, + showInNavMenus = true, + showInQuickEdit = true + ) + + val categoryDetails = TaxonomyTypeDetailsWithEditContext( + name = "Categories", + slug = "category", + description = "Test categories", + visibility = visibility, + restBase = "categories", + restNamespace = "wp/v2", + types = listOf("post"), + hierarchical = true, + showCloud = true, + capabilities = mock(), + labels = mock() + ) + + val tagDetails = TaxonomyTypeDetailsWithEditContext( + name = "Tags", + slug = "post_tag", + description = "Test tags", + visibility = visibility, + restBase = "tags", + restNamespace = "wp/v2", + types = listOf("post"), + hierarchical = false, + showCloud = true, + capabilities = mock(), + labels = mock() + ) + + return TaxonomyTypesResponseWithEditContext( + mapOf( + TaxonomyType.Category to categoryDetails, + TaxonomyType.PostTag to tagDetails + ) + ) + } +}