From 5c81978ba8c232403e270f2d6204cdfcc45363e9 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 11:03:38 -0400 Subject: [PATCH 1/8] task: show post type labels in the demo apps Slugs and REST bases read poorly once capitalized, e.g. "Templated-Posts". The labels match what WP Admin shows. The slug remains the fallback when the post types weren't loaded from the site. Also fixes the "No Entires" typo. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YNDBX7RNXn3NsfDuV1PkNV --- .../gutenbergkit/SitePreparationActivity.kt | 5 ++- .../gutenbergkit/SitePreparationViewModel.kt | 16 ++++++--- .../Sources/Views/PostsListView.swift | 15 +++++++-- .../Sources/Views/SitePreparationView.swift | 33 ++++++++++++++----- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt index c999bbce0..8f4b58dec 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt @@ -332,6 +332,7 @@ private fun LoadedView( enableNetworkLogging = uiState.enableNetworkLogging, onEnableNetworkLoggingChange = viewModel::setEnableNetworkLogging, postTypes = uiState.postTypes, + postTypeLabels = uiState.postTypeLabels, selectedPostType = uiState.selectedPostType, onPostTypeChange = viewModel::setPostType, showBrowseButton = accountId != null, @@ -427,6 +428,7 @@ private fun FeatureConfigurationCard( enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List, + postTypeLabels: Map, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, @@ -566,7 +568,8 @@ private fun FeatureConfigurationCard( enabled = !isPreparing ) Text( - text = postType.postType.replaceFirstChar { it.uppercase() }, + text = postTypeLabels[postType.postType] + ?: postType.postType.replaceFirstChar { it.uppercase() }, modifier = Modifier.padding(start = 8.dp) ) } diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index 740f155fc..76f05c7a8 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -29,6 +29,8 @@ data class SitePreparationUiState( val enableNetworkLogging: Boolean = false, /** All viewable post types fetched from the site, or empty while loading. */ val postTypes: List = emptyList(), + /** The singular label of each post type, keyed by slug. Empty when not fetched from the site. */ + val postTypeLabels: Map = emptyMap(), /** The post type currently selected in the picker. Null until [postTypes] loads. */ val selectedPostType: PostTypeDetails? = null, val cacheBundleCount: Int? = null, @@ -274,11 +276,15 @@ class SitePreparationViewModel( // available (the typical case); otherwise pick the first type in the // list. Falls back to `PostTypeDetails.post` if the call fails so the // editor can still launch with a sensible default. - val postTypes = loadPostTypes(config) ?: listOf(PostTypeDetails.post) + val loadedPostTypes = loadPostTypes(config) + val postTypes = loadedPostTypes?.map { it.first } ?: listOf(PostTypeDetails.post) val defaultPostType = postTypes.firstOrNull { it.postType == "post" } ?: postTypes.first() _uiState.update { it.copy( postTypes = postTypes, + postTypeLabels = loadedPostTypes + ?.associate { (postType, label) -> postType.postType to label } + .orEmpty(), selectedPostType = defaultPostType ) } @@ -310,7 +316,7 @@ class SitePreparationViewModel( /** * Fetches the site's post types from the WordPress REST API and returns the - * subset relevant to the editor picker. + * subset relevant to the editor picker, each with its singular label. * * Mirrors the iOS [SitePreparationView.loadPostTypes] filter: * - always include the standard `post` and `page` types @@ -322,7 +328,7 @@ class SitePreparationViewModel( */ private suspend fun loadPostTypes( config: ConfigurationItem.ConfiguredEditor - ): List? { + ): List>? { val app = getApplication() val account = app.accountRepository.all().firstOrNull { it.id() == config.accountId } ?: return null @@ -346,9 +352,9 @@ class SitePreparationViewModel( postType = details.slug, restBase = details.restBase, restNamespace = details.restNamespace - ) + ) to details.labels.singularName } - .sortedBy { it.postType } + .sortedBy { it.first.postType } } /** diff --git a/ios/Demo-iOS/Sources/Views/PostsListView.swift b/ios/Demo-iOS/Sources/Views/PostsListView.swift index f0f3331aa..8e81f5651 100644 --- a/ios/Demo-iOS/Sources/Views/PostsListView.swift +++ b/ios/Demo-iOS/Sources/Views/PostsListView.swift @@ -10,7 +10,16 @@ struct PostsListView: View { @State private var viewModel: PostsListViewModel - init(client: WordPressAPI, postTypeDetails: PostTypeDetails, editorConfiguration: EditorConfiguration, editorDependencies: EditorDependencies?) { + private let title: String + + init( + client: WordPressAPI, + postTypeDetails: PostTypeDetails, + title: String, + editorConfiguration: EditorConfiguration, + editorDependencies: EditorDependencies? + ) { + self.title = title self.viewModel = PostsListViewModel( client: client, postTypeDetails: postTypeDetails, @@ -37,7 +46,7 @@ struct PostsListView: View { } } else if viewModel.posts.isEmpty { ContentUnavailableView { - Label("No Entires", systemImage: "doc.text") + Label("No Entries", systemImage: "doc.text") } description: { Text("No entries found for this post type.") } @@ -63,7 +72,7 @@ struct PostsListView: View { .listStyle(.plain) } } - .navigationTitle(viewModel.postTypeDetails.restBase.capitalized) + .navigationTitle(title) .task { await viewModel.loadPosts() } diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index 94a6477d2..49cf812fe 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -76,7 +76,7 @@ struct SitePreparationView: View { } else { Picker("Post Type", selection: $viewModel.selectedPostTypeDetails) { ForEach(viewModel.postTypes, id: \.self) { postType in - Text(postType.name).tag(postType) + Text(viewModel.label(for: postType)).tag(postType) } } .disabled(viewModel.disableButtons) @@ -87,6 +87,7 @@ struct SitePreparationView: View { PostsListView( client: client, postTypeDetails: viewModel.selectedPostTypeDetails, + title: viewModel.pluralLabel(for: viewModel.selectedPostTypeDetails), editorConfiguration: configuration, editorDependencies: viewModel.editorDependencies ) @@ -245,6 +246,9 @@ class SitePreparationViewModel { var postTypes: [PostTypeDetails] = [] + /// The singular and plural names of each post type, keyed by slug. + var postTypeLabels: [String: (singular: String, plural: String)] = [:] + var selectedPostTypeDetails: PostTypeDetails { get { editorConfiguration?.postType ?? .post } set { @@ -532,7 +536,7 @@ class SitePreparationViewModel { let response = try await client.postTypes.listWithEditContext().data - self.postTypes = response.postTypes + let editablePostTypes = response.postTypes .filter { type, details in switch type { case .post, .page: @@ -546,6 +550,13 @@ class SitePreparationViewModel { return details.viewable && details.visibility.showUi } .values + + self.postTypeLabels = Dictionary( + uniqueKeysWithValues: editablePostTypes.map { + ($0.slug, (singular: $0.labels.singularName, plural: $0.labels.name)) + } + ) + self.postTypes = editablePostTypes .map { postType in PostTypeDetails( postType: postType.slug, @@ -560,6 +571,18 @@ class SitePreparationViewModel { } } + /// The singular name of a post type, falling back to its slug when the post + /// types were not loaded from the site. + func label(for postType: PostTypeDetails) -> String { + postTypeLabels[postType.postType]?.singular ?? postType.postType.capitalized + } + + /// The plural name of a post type, falling back to its REST base when the + /// post types were not loaded from the site. + func pluralLabel(for postType: PostTypeDetails) -> String { + postTypeLabels[postType.postType]?.plural ?? postType.restBase.capitalized + } + func buildAndLoadConfiguration(navigation: Navigation) { guard let configuration = self.editorConfiguration else { preconditionFailure("Unable to build configuration without editor configuration – the UI should prevent this") @@ -613,12 +636,6 @@ struct KeyValueRow: View { } } -extension PostTypeDetails { - var name: String { - postType.capitalized - } -} - #Preview("Bundled Editor") { NavigationStack { SitePreparationView(site: .bundledEditor) From a2620f82ffb2fd3ca332333843f663902deaeee9 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 11:17:44 -0400 Subject: [PATCH 2/8] task: keep the post type picker in the detekt baseline Detekt identifies baselined issues by the function signature, so adding the labels parameter resurfaced the card's existing LongMethod and LongParameterList issues. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YNDBX7RNXn3NsfDuV1PkNV --- android/app/detekt-baseline.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/detekt-baseline.xml b/android/app/detekt-baseline.xml index 88a28094f..ebc1e0245 100644 --- a/android/app/detekt-baseline.xml +++ b/android/app/detekt-baseline.xml @@ -6,9 +6,9 @@ LongMethod:MainActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) LongMethod:MediaProxyServerActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaProxyServerScreen(onBack: () -> Unit) LongMethod:PostsListActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostsListScreen( viewModel: PostsListViewModel, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) - LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) + LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, postTypeLabels: Map<String, String>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) LongParameterList:MainActivity.kt$( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) - LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) + LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, postTypeLabels: Map<String, String>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) MaxLineLength:MediaProxyServerActivity.kt$Text("Size", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) MaxLineLength:MediaProxyServerActivity.kt$Text("Throughput", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) MaxLineLength:MediaProxyServerActivity.kt$Text("Time", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) From 9f6cdc234411363c03decd6d9ddc9b440a0aa4c2 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 12:10:12 -0400 Subject: [PATCH 3/8] refactor: remove the iOS demo's unreachable post type default The assignment ran before the editor configuration existed, so the setter returned early and the default was always `post`. Co-Authored-By: Claude Opus 5.5 --- ios/Demo-iOS/Sources/Views/SitePreparationView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index 49cf812fe..dc03ee50d 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -565,10 +565,6 @@ class SitePreparationViewModel { ) } .sorted(using: KeyPathComparator(\.postType)) - - if let firstType = postTypes.first { - self.selectedPostTypeDetails = firstType - } } /// The singular name of a post type, falling back to its slug when the post From f83f70cd77952ffdad0b1d7aa6ce7f4a80be91c4 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 12:14:39 -0400 Subject: [PATCH 4/8] refactor: pair demo post types with their labels Storing labels in a slug-keyed map beside the post types let the two drift and spread the fallback across call sites. Keeping each label on its post type resolves the fallback once and drops the dictionary that trapped on duplicate slugs. Co-Authored-By: Claude Opus 5.5 --- android/app/detekt-baseline.xml | 4 +- .../gutenbergkit/SitePreparationActivity.kt | 13 ++-- .../gutenbergkit/SitePreparationViewModel.kt | 53 ++++++++------ .../Sources/Views/SitePreparationView.swift | 71 ++++++++++--------- 4 files changed, 75 insertions(+), 66 deletions(-) diff --git a/android/app/detekt-baseline.xml b/android/app/detekt-baseline.xml index ebc1e0245..5263459ce 100644 --- a/android/app/detekt-baseline.xml +++ b/android/app/detekt-baseline.xml @@ -6,9 +6,9 @@ LongMethod:MainActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) LongMethod:MediaProxyServerActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaProxyServerScreen(onBack: () -> Unit) LongMethod:PostsListActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostsListScreen( viewModel: PostsListViewModel, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) - LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, postTypeLabels: Map<String, String>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) + LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeOption>, selectedPostType: PostTypeOption?, onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) LongParameterList:MainActivity.kt$( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) - LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, postTypeLabels: Map<String, String>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) + LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeOption>, selectedPostType: PostTypeOption?, onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) MaxLineLength:MediaProxyServerActivity.kt$Text("Size", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) MaxLineLength:MediaProxyServerActivity.kt$Text("Throughput", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) MaxLineLength:MediaProxyServerActivity.kt$Text("Time", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt index 8f4b58dec..bf691b120 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt @@ -267,7 +267,7 @@ fun SitePreparationScreen( val selectedPostType = uiState.selectedPostType if (selectedPostType != null) { viewModel.buildConfiguration()?.let { config -> - onBrowsePosts(config, uiState.editorDependencies, selectedPostType) + onBrowsePosts(config, uiState.editorDependencies, selectedPostType.details) } } }, @@ -332,7 +332,6 @@ private fun LoadedView( enableNetworkLogging = uiState.enableNetworkLogging, onEnableNetworkLoggingChange = viewModel::setEnableNetworkLogging, postTypes = uiState.postTypes, - postTypeLabels = uiState.postTypeLabels, selectedPostType = uiState.selectedPostType, onPostTypeChange = viewModel::setPostType, showBrowseButton = accountId != null, @@ -427,10 +426,9 @@ private fun FeatureConfigurationCard( isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, - postTypes: List, - postTypeLabels: Map, - selectedPostType: PostTypeDetails?, - onPostTypeChange: (PostTypeDetails) -> Unit, + postTypes: List, + selectedPostType: PostTypeOption?, + onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) { @@ -568,8 +566,7 @@ private fun FeatureConfigurationCard( enabled = !isPreparing ) Text( - text = postTypeLabels[postType.postType] - ?: postType.postType.replaceFirstChar { it.uppercase() }, + text = postType.label, modifier = Modifier.padding(start = 8.dp) ) } diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index 76f05c7a8..8d9d7d961 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -28,11 +28,9 @@ data class SitePreparationUiState( val enableThirdPartyBlocks: Boolean = true, val enableNetworkLogging: Boolean = false, /** All viewable post types fetched from the site, or empty while loading. */ - val postTypes: List = emptyList(), - /** The singular label of each post type, keyed by slug. Empty when not fetched from the site. */ - val postTypeLabels: Map = emptyMap(), + val postTypes: List = emptyList(), /** The post type currently selected in the picker. Null until [postTypes] loads. */ - val selectedPostType: PostTypeDetails? = null, + val selectedPostType: PostTypeOption? = null, val cacheBundleCount: Int? = null, val isLoading: Boolean = false, val error: String? = null, @@ -41,6 +39,17 @@ data class SitePreparationUiState( val loadingProgress: Float? = null ) +/** A post type offered in the picker, with the name shown for it. */ +data class PostTypeOption(val details: PostTypeDetails, val label: String) { + companion object { + /** Falls back to the slug when the site's label is unavailable. */ + fun from(details: PostTypeDetails, label: String? = null) = PostTypeOption( + details = details, + label = label ?: details.postType.replaceFirstChar { it.uppercase() } + ) + } +} + class SitePreparationViewModel( application: Application, private val configurationItem: ConfigurationItem @@ -115,7 +124,7 @@ class SitePreparationViewModel( _uiState.update { it.copy(enableNetworkLogging = enabled) } } - fun setPostType(postType: PostTypeDetails) { + fun setPostType(postType: PostTypeOption) { // Discard dependencies prepared for the previous post type. _uiState.update { it.copy(selectedPostType = postType, editorDependencies = null) } } @@ -229,10 +238,11 @@ class SitePreparationViewModel( private fun createBundledConfiguration(): EditorConfiguration { // Bundled offline editor: only the standard `post` type is meaningful. + val post = PostTypeOption.from(PostTypeDetails.post) _uiState.update { it.copy( - postTypes = listOf(PostTypeDetails.post), - selectedPostType = PostTypeDetails.post + postTypes = listOf(post), + selectedPostType = post ) } return EditorConfiguration.builder( @@ -276,15 +286,11 @@ class SitePreparationViewModel( // available (the typical case); otherwise pick the first type in the // list. Falls back to `PostTypeDetails.post` if the call fails so the // editor can still launch with a sensible default. - val loadedPostTypes = loadPostTypes(config) - val postTypes = loadedPostTypes?.map { it.first } ?: listOf(PostTypeDetails.post) - val defaultPostType = postTypes.firstOrNull { it.postType == "post" } ?: postTypes.first() + val postTypes = loadPostTypes(config) ?: listOf(PostTypeOption.from(PostTypeDetails.post)) + val defaultPostType = postTypes.firstOrNull { it.details.postType == "post" } ?: postTypes.first() _uiState.update { it.copy( postTypes = postTypes, - postTypeLabels = loadedPostTypes - ?.associate { (postType, label) -> postType.postType to label } - .orEmpty(), selectedPostType = defaultPostType ) } @@ -292,7 +298,7 @@ class SitePreparationViewModel( return EditorConfiguration.builder( siteURL = config.siteUrl, siteApiRoot = siteApiRoot, - postType = defaultPostType + postType = defaultPostType.details ) .setPlugins(capabilities.supportsPlugins) .setThemeStyles(capabilities.supportsThemeStyles) @@ -328,7 +334,7 @@ class SitePreparationViewModel( */ private suspend fun loadPostTypes( config: ConfigurationItem.ConfiguredEditor - ): List>? { + ): List? { val app = getApplication() val account = app.accountRepository.all().firstOrNull { it.id() == config.accountId } ?: return null @@ -348,13 +354,16 @@ class SitePreparationViewModel( } } .map { (_, details) -> - PostTypeDetails( - postType = details.slug, - restBase = details.restBase, - restNamespace = details.restNamespace - ) to details.labels.singularName + PostTypeOption.from( + details = PostTypeDetails( + postType = details.slug, + restBase = details.restBase, + restNamespace = details.restNamespace + ), + label = details.labels.singularName + ) } - .sortedBy { it.first.postType } + .sortedBy { it.details.postType } } /** @@ -378,7 +387,7 @@ class SitePreparationViewModel( .setEnableInserterMediaStrip(_uiState.value.enableInserterMediaStrip) .setThemeStyles(baseConfig.themeStyles && _uiState.value.enableThemeStyles) .setPlugins(baseConfig.plugins && _uiState.value.enableThirdPartyBlocks) - .setPostType(selectedPostType) + .setPostType(selectedPostType.details) .build() } } diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index dc03ee50d..be747f0de 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -75,8 +75,8 @@ struct SitePreparationView: View { } } else { Picker("Post Type", selection: $viewModel.selectedPostTypeDetails) { - ForEach(viewModel.postTypes, id: \.self) { postType in - Text(viewModel.label(for: postType)).tag(postType) + ForEach(viewModel.postTypes, id: \.details) { option in + Text(option.label).tag(option.details) } } .disabled(viewModel.disableButtons) @@ -87,7 +87,7 @@ struct SitePreparationView: View { PostsListView( client: client, postTypeDetails: viewModel.selectedPostTypeDetails, - title: viewModel.pluralLabel(for: viewModel.selectedPostTypeDetails), + title: viewModel.selectedPostType.pluralLabel, editorConfiguration: configuration, editorDependencies: viewModel.editorDependencies ) @@ -244,10 +244,12 @@ class SitePreparationViewModel { } } - var postTypes: [PostTypeDetails] = [] + var postTypes: [PostTypeOption] = [] - /// The singular and plural names of each post type, keyed by slug. - var postTypeLabels: [String: (singular: String, plural: String)] = [:] + var selectedPostType: PostTypeOption { + postTypes.first { $0.details == selectedPostTypeDetails } + ?? PostTypeOption(details: selectedPostTypeDetails) + } var selectedPostTypeDetails: PostTypeDetails { get { editorConfiguration?.postType ?? .post } @@ -292,7 +294,7 @@ class SitePreparationViewModel { switch configurationItem { case .bundledEditor: self.editorConfiguration = Self.applyDemoAppDefaults(to: .bundled) - self.postTypes = [.post, .page] + self.postTypes = PostTypeOption.builtIn case .localWordPress: guard let credentials = LocalWordPressCredentials.load() else { throw AppError(errorDescription: "Local WordPress not configured.\n\nRun 'make wp-env-start' from the project root to set up a local WordPress environment.") @@ -349,7 +351,7 @@ class SitePreparationViewModel { let newConfiguration = try await self.loadConfiguration(for: account) self.editorConfiguration = Self.applyDemoAppDefaults(to: newConfiguration) } catch let error as WpApiError where error.isDeviceOffline { - self.postTypes = [.post, .page] + self.postTypes = PostTypeOption.builtIn let fallback = Self.buildOfflineConfiguration(for: account) self.editorConfiguration = Self.applyDemoAppDefaults(to: fallback) } @@ -528,7 +530,7 @@ class SitePreparationViewModel { @MainActor private func loadPostTypes() async throws { guard let client = self.client else { - self.postTypes = [.post, .page] + self.postTypes = PostTypeOption.builtIn return } @@ -536,7 +538,7 @@ class SitePreparationViewModel { let response = try await client.postTypes.listWithEditContext().data - let editablePostTypes = response.postTypes + self.postTypes = response.postTypes .filter { type, details in switch type { case .post, .page: @@ -550,33 +552,18 @@ class SitePreparationViewModel { return details.viewable && details.visibility.showUi } .values - - self.postTypeLabels = Dictionary( - uniqueKeysWithValues: editablePostTypes.map { - ($0.slug, (singular: $0.labels.singularName, plural: $0.labels.name)) - } - ) - self.postTypes = editablePostTypes .map { postType in - PostTypeDetails( - postType: postType.slug, - restBase: postType.restBase, - restNamespace: postType.restNamespace + PostTypeOption( + details: PostTypeDetails( + postType: postType.slug, + restBase: postType.restBase, + restNamespace: postType.restNamespace + ), + label: postType.labels.singularName, + pluralLabel: postType.labels.name ) } - .sorted(using: KeyPathComparator(\.postType)) - } - - /// The singular name of a post type, falling back to its slug when the post - /// types were not loaded from the site. - func label(for postType: PostTypeDetails) -> String { - postTypeLabels[postType.postType]?.singular ?? postType.postType.capitalized - } - - /// The plural name of a post type, falling back to its REST base when the - /// post types were not loaded from the site. - func pluralLabel(for postType: PostTypeDetails) -> String { - postTypeLabels[postType.postType]?.plural ?? postType.restBase.capitalized + .sorted(using: KeyPathComparator(\.details.postType)) } func buildAndLoadConfiguration(navigation: Navigation) { @@ -594,6 +581,22 @@ class SitePreparationViewModel { } } +/// A post type offered in the picker, with the names shown for it. +struct PostTypeOption { + static let builtIn = [PostTypeOption(details: .post), PostTypeOption(details: .page)] + + let details: PostTypeDetails + let label: String + let pluralLabel: String + + /// Falls back to the slug and REST base when the site's labels are unavailable. + init(details: PostTypeDetails, label: String? = nil, pluralLabel: String? = nil) { + self.details = details + self.label = label ?? details.postType.capitalized + self.pluralLabel = pluralLabel ?? details.restBase.capitalized + } +} + struct KeyValueRow: View { enum Value { From cb9e56e5fa34f5a67c1f4439aa3e489218149137 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 12:14:53 -0400 Subject: [PATCH 5/8] task: fall back when a demo post type label is blank A post type registered with an empty label otherwise renders a blank picker row and posts-list title. Co-Authored-By: Claude Opus 5.5 --- .../example/gutenbergkit/SitePreparationViewModel.kt | 4 ++-- ios/Demo-iOS/Sources/Views/SitePreparationView.swift | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index 8d9d7d961..28ce4aadc 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -42,10 +42,10 @@ data class SitePreparationUiState( /** A post type offered in the picker, with the name shown for it. */ data class PostTypeOption(val details: PostTypeDetails, val label: String) { companion object { - /** Falls back to the slug when the site's label is unavailable. */ + /** Falls back to the slug when the site's label is missing or blank. */ fun from(details: PostTypeDetails, label: String? = null) = PostTypeOption( details = details, - label = label ?: details.postType.replaceFirstChar { it.uppercase() } + label = label?.ifBlank { null } ?: details.postType.replaceFirstChar { it.uppercase() } ) } } diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index be747f0de..0a4eb7ae5 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -589,11 +589,16 @@ struct PostTypeOption { let label: String let pluralLabel: String - /// Falls back to the slug and REST base when the site's labels are unavailable. + /// Falls back to the slug and REST base when the site's labels are missing or blank. init(details: PostTypeDetails, label: String? = nil, pluralLabel: String? = nil) { self.details = details - self.label = label ?? details.postType.capitalized - self.pluralLabel = pluralLabel ?? details.restBase.capitalized + self.label = Self.nonBlank(label) ?? details.postType.capitalized + self.pluralLabel = Self.nonBlank(pluralLabel) ?? details.restBase.capitalized + } + + private static func nonBlank(_ value: String?) -> String? { + guard let value, !value.trimmingCharacters(in: .whitespaces).isEmpty else { return nil } + return value } } From a2d4eae876e921baff740043bc5c2662a382aef4 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 12:15:00 -0400 Subject: [PATCH 6/8] task: sort the demo post type picker by label The picker shows labels, so sorting by slug read as random (e.g. "Templated Post, Page, Post"). Co-Authored-By: Claude Opus 5.5 --- .../java/com/example/gutenbergkit/SitePreparationViewModel.kt | 2 +- ios/Demo-iOS/Sources/Views/SitePreparationView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index 28ce4aadc..d71b38665 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -363,7 +363,7 @@ class SitePreparationViewModel( label = details.labels.singularName ) } - .sortedBy { it.details.postType } + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) } /** diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index 0a4eb7ae5..6e7a705b1 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -563,7 +563,7 @@ class SitePreparationViewModel { pluralLabel: postType.labels.name ) } - .sorted(using: KeyPathComparator(\.details.postType)) + .sorted(using: KeyPathComparator(\.label, comparator: .localizedStandard)) } func buildAndLoadConfiguration(navigation: Navigation) { From 3c68105fd09966d88c51cbc42a1f647fb6a0f715 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 22 Oct 2026 12:15:43 -0400 Subject: [PATCH 7/8] task: title the Android demo posts list with the plural label Matches the iOS posts list, which already shows e.g. "Pages" instead of a hard-coded "Posts". Co-Authored-By: Claude Opus 5.5 --- android/app/detekt-baseline.xml | 2 +- .../example/gutenbergkit/PostsListActivity.kt | 8 +++++++- .../gutenbergkit/SitePreparationActivity.kt | 16 +++++++++++----- .../gutenbergkit/SitePreparationViewModel.kt | 16 +++++++++------- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/android/app/detekt-baseline.xml b/android/app/detekt-baseline.xml index 5263459ce..2faf7416e 100644 --- a/android/app/detekt-baseline.xml +++ b/android/app/detekt-baseline.xml @@ -5,7 +5,7 @@ LongMethod:EditorActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun EditorScreen( configuration: EditorConfiguration, dependencies: EditorDependencies? = null, accountId: ULong? = null, enableNativeMediaUpload: Boolean = true, coroutineScope: CoroutineScope, onClose: () -> Unit, onGutenbergViewCreated: (GutenbergView) -> Unit = {} ) LongMethod:MainActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) LongMethod:MediaProxyServerActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaProxyServerScreen(onBack: () -> Unit) - LongMethod:PostsListActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostsListScreen( viewModel: PostsListViewModel, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) + LongMethod:PostsListActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostsListScreen( viewModel: PostsListViewModel, title: String, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeOption>, selectedPostType: PostTypeOption?, onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) LongParameterList:MainActivity.kt$( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeOption>, selectedPostType: PostTypeOption?, onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) diff --git a/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt index 933c07c04..efc079b8a 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt @@ -68,17 +68,20 @@ class PostsListActivity : ComponentActivity() { companion object { const val EXTRA_ACCOUNT_ID = "account_id" const val EXTRA_POST_TYPE = "post_type" + const val EXTRA_TITLE = "title" fun createIntent( context: Context, accountId: ULong, postType: PostTypeDetails, + title: String, configuration: EditorConfiguration, dependencies: EditorDependencies? ): Intent { return Intent(context, PostsListActivity::class.java).apply { putExtra(EXTRA_ACCOUNT_ID, accountId.toLong()) putExtra(EXTRA_POST_TYPE, postType) + putExtra(EXTRA_TITLE, title) putExtra(MainActivity.EXTRA_CONFIGURATION, configuration) if (dependencies != null) { val filePath = EditorDependenciesSerializer.writeToDisk(context, dependencies) @@ -95,6 +98,7 @@ class PostsListActivity : ComponentActivity() { val accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1L).takeIf { it >= 0 }?.toULong() val postType = intent.getParcelableExtra(EXTRA_POST_TYPE, PostTypeDetails::class.java) ?: PostTypeDetails.post + val title = intent.getStringExtra(EXTRA_TITLE) ?: getString(R.string.posts) val configuration = intent.getParcelableExtra(MainActivity.EXTRA_CONFIGURATION, EditorConfiguration::class.java) val dependenciesPath = intent.getStringExtra(EditorActivity.EXTRA_DEPENDENCIES_PATH) @@ -116,6 +120,7 @@ class PostsListActivity : ComponentActivity() { AppTheme { PostsListScreen( viewModel = viewModel, + title = title, onClose = { finish() }, onPostSelected = { post -> launchEditor(post, configuration, dependenciesPath, accountId) @@ -209,6 +214,7 @@ class PostsListViewModel( @Composable fun PostsListScreen( viewModel: PostsListViewModel, + title: String, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) { @@ -222,7 +228,7 @@ fun PostsListScreen( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( - title = { Text(stringResource(R.string.posts)) }, + title = { Text(title) }, navigationIcon = { IconButton(onClick = onClose) { Icon( diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt index bf691b120..6ae8449a7 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt @@ -70,7 +70,6 @@ import java.util.Locale import org.wordpress.gutenberg.model.EditorConfiguration import org.wordpress.gutenberg.model.EditorDependencies import org.wordpress.gutenberg.model.EditorDependenciesSerializer -import org.wordpress.gutenberg.model.PostTypeDetails class SitePreparationActivity : ComponentActivity() { @@ -197,12 +196,19 @@ class SitePreparationActivity : ComponentActivity() { private fun launchPostsList( accountId: ULong, - postType: PostTypeDetails, + postType: PostTypeOption, configuration: EditorConfiguration, dependencies: EditorDependencies? ) { startActivity( - PostsListActivity.createIntent(this, accountId, postType, configuration, dependencies) + PostsListActivity.createIntent( + this, + accountId, + postType.details, + postType.pluralLabel, + configuration, + dependencies + ) ) } } @@ -214,7 +220,7 @@ fun SitePreparationScreen( accountId: ULong?, onClose: () -> Unit, onStartEditor: (EditorConfiguration, EditorDependencies?, Boolean) -> Unit, - onBrowsePosts: (EditorConfiguration, EditorDependencies?, PostTypeDetails) -> Unit + onBrowsePosts: (EditorConfiguration, EditorDependencies?, PostTypeOption) -> Unit ) { val uiState by viewModel.uiState.collectAsState() @@ -267,7 +273,7 @@ fun SitePreparationScreen( val selectedPostType = uiState.selectedPostType if (selectedPostType != null) { viewModel.buildConfiguration()?.let { config -> - onBrowsePosts(config, uiState.editorDependencies, selectedPostType.details) + onBrowsePosts(config, uiState.editorDependencies, selectedPostType) } } }, diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index d71b38665..469cc2c95 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -39,13 +39,14 @@ data class SitePreparationUiState( val loadingProgress: Float? = null ) -/** A post type offered in the picker, with the name shown for it. */ -data class PostTypeOption(val details: PostTypeDetails, val label: String) { +/** A post type offered in the picker, with the names shown for it. */ +data class PostTypeOption(val details: PostTypeDetails, val label: String, val pluralLabel: String) { companion object { - /** Falls back to the slug when the site's label is missing or blank. */ - fun from(details: PostTypeDetails, label: String? = null) = PostTypeOption( + /** Falls back to the slug and REST base when the site's labels are missing or blank. */ + fun from(details: PostTypeDetails, label: String? = null, pluralLabel: String? = null) = PostTypeOption( details = details, - label = label?.ifBlank { null } ?: details.postType.replaceFirstChar { it.uppercase() } + label = label?.ifBlank { null } ?: details.postType.replaceFirstChar { it.uppercase() }, + pluralLabel = pluralLabel?.ifBlank { null } ?: details.restBase.replaceFirstChar { it.uppercase() } ) } } @@ -322,7 +323,7 @@ class SitePreparationViewModel( /** * Fetches the site's post types from the WordPress REST API and returns the - * subset relevant to the editor picker, each with its singular label. + * subset relevant to the editor picker, each with its singular and plural labels. * * Mirrors the iOS [SitePreparationView.loadPostTypes] filter: * - always include the standard `post` and `page` types @@ -360,7 +361,8 @@ class SitePreparationViewModel( restBase = details.restBase, restNamespace = details.restNamespace ), - label = details.labels.singularName + label = details.labels.singularName, + pluralLabel = details.labels.name ) } .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) From 1b8c139ec5bbf565cf74df24bbe3f1748d33ca61 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 23 Oct 2026 09:39:57 -0400 Subject: [PATCH 8/8] task: break demo post type picker label ties by slug Labels aren't unique (an unlabeled custom type inherits "Post" or "Page"), and the REST response map's order is randomly seeded, so tied rows reordered between launches. Co-Authored-By: Claude Opus 5.5 --- .../com/example/gutenbergkit/SitePreparationViewModel.kt | 5 ++++- ios/Demo-iOS/Sources/Views/SitePreparationView.swift | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index 469cc2c95..b71615e04 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -365,7 +365,10 @@ class SitePreparationViewModel( pluralLabel = details.labels.name ) } - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) + .sortedWith( + compareBy(String.CASE_INSENSITIVE_ORDER) { it.label } + .thenBy { it.details.postType } + ) } /** diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index 6e7a705b1..1e2fc875e 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -563,7 +563,10 @@ class SitePreparationViewModel { pluralLabel: postType.labels.name ) } - .sorted(using: KeyPathComparator(\.label, comparator: .localizedStandard)) + .sorted(using: [ + KeyPathComparator(\.label, comparator: .localizedStandard), + KeyPathComparator(\.details.postType), + ]) } func buildAndLoadConfiguration(navigation: Navigation) {