Turn dead-end empty states into on-ramps by wiring the existing EmptyState actionLabel/onAction params to real next actions. Screens wired (real, existing actions): - Skills (installed): Browse skill hub -> setViewMode(HUB) - Skills (hub): Retry search -> searchHub(hubQuery) - Cron Jobs: Create your first job -> openNewJobDialog() - MCP servers: Add a server -> toggleAddForm() (literal moved to strings.xml) - Webhooks: Create subscription -> showCreateDialog() (literal moved to strings.xml) - Channels: Connect a Telegram channel -> startTelegramOnboarding() - Model: Configure model -> navigateTo(SettingsConnection) - Sessions (history): Start a new chat -> navigateTo(ChatScreen) - Analytics: Explore Skills or Cron jobs -> navigateTo(SkillsScreen) - Keys (no match): Clear search -> query = "" All new labels use strings.xml (empty_action_* / screenname_empty_*). Deviations from issue (documented honestly): - Toolsets, Profiles, Plugins keep Refresh. No in-app create path exists (toolsets are server-defined toggles; profiles/plugins have no create dialog wired in UI). Refresh is the correct recovery there, not a fake 'create' CTA. Flagged for follow-up if create flows get built. - Skills: edited the real private InstalledSkillsView in SkillsScreen.kt; the duplicate internal one in InstalledSkillsTab.kt is dead code. Verified: ktlintCheck clean, assembleDebug green. Co-authored-by: M57 <hy4ri@users.noreply.github.com> Co-authored-by: Ronia <ronia@hermes>
This commit is contained in:
11 files changed
+59
-7
No files matched your search
@@ -39,7 +39,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.NavigationController
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.SkillsScreen
|
||||||
import com.m57.hermescontrol.data.model.AnalyticsDailyEntry
|
import com.m57.hermescontrol.data.model.AnalyticsDailyEntry
|
||||||
import com.m57.hermescontrol.data.model.ModelsAnalyticsModelEntry
|
import com.m57.hermescontrol.data.model.ModelsAnalyticsModelEntry
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
@@ -89,6 +91,8 @@ fun AnalyticsScreen(
|
|||||||
title = stringResource(R.string.analytics_empty_title),
|
title = stringResource(R.string.analytics_empty_title),
|
||||||
subtitle = stringResource(R.string.analytics_empty_desc),
|
subtitle = stringResource(R.string.analytics_empty_desc),
|
||||||
icon = Icons.Filled.BarChart,
|
icon = Icons.Filled.BarChart,
|
||||||
|
actionLabel = stringResource(R.string.empty_action_explore),
|
||||||
|
onAction = { NavigationController.navigateTo(SkillsScreen) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ fun ChannelsScreen(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
title = stringResource(R.string.channels_empty_title),
|
title = stringResource(R.string.channels_empty_title),
|
||||||
subtitle = stringResource(R.string.channels_empty_desc),
|
subtitle = stringResource(R.string.channels_empty_desc),
|
||||||
|
actionLabel = stringResource(R.string.empty_action_connect_telegram),
|
||||||
|
onAction = { viewModel.startTelegramOnboarding() },
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ fun CronJobsScreen(
|
|||||||
icon = Icons.Filled.Schedule,
|
icon = Icons.Filled.Schedule,
|
||||||
title = stringResource(R.string.cron_empty_title),
|
title = stringResource(R.string.cron_empty_title),
|
||||||
subtitle = stringResource(R.string.cron_empty_desc),
|
subtitle = stringResource(R.string.cron_empty_desc),
|
||||||
|
actionLabel = stringResource(R.string.empty_action_create_job),
|
||||||
|
onAction = { viewModel.openNewJobDialog() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,8 +158,10 @@ fun KeysScreen(
|
|||||||
if (filteredCategories.isEmpty() && query.isNotBlank()) {
|
if (filteredCategories.isEmpty() && query.isNotBlank()) {
|
||||||
item(key = "no-results") {
|
item(key = "no-results") {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No matching keys",
|
title = stringResource(R.string.keys_no_matching_title),
|
||||||
subtitle = "Try a different search term.",
|
subtitle = stringResource(R.string.keys_no_matching_desc),
|
||||||
|
actionLabel = stringResource(R.string.empty_action_clear_search),
|
||||||
|
onAction = { query = "" },
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,8 +155,10 @@ fun McpServersScreen(
|
|||||||
if (filteredServers.isEmpty() && query.isEmpty()) {
|
if (filteredServers.isEmpty() && query.isEmpty()) {
|
||||||
item(key = "empty") {
|
item(key = "empty") {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No MCP servers",
|
title = stringResource(R.string.mcp_servers_empty_title),
|
||||||
subtitle = "Add a server above or from the catalog.",
|
subtitle = stringResource(R.string.mcp_servers_empty_desc),
|
||||||
|
actionLabel = stringResource(R.string.empty_action_add_server),
|
||||||
|
onAction = { viewModel.toggleAddForm() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (filteredServers.isEmpty()) {
|
} else if (filteredServers.isEmpty()) {
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.NavigationController
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.SettingsConnection
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
@@ -116,8 +118,8 @@ fun ModelScreen(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
title = stringResource(R.string.model_empty_title),
|
title = stringResource(R.string.model_empty_title),
|
||||||
subtitle = stringResource(R.string.model_empty_desc),
|
subtitle = stringResource(R.string.model_empty_desc),
|
||||||
onAction = { viewModel.loadAll() },
|
actionLabel = stringResource(R.string.empty_action_configure_model),
|
||||||
actionLabel = stringResource(R.string.content_desc_refresh),
|
onAction = { NavigationController.navigateTo(SettingsConnection) },
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -595,6 +595,8 @@ fun SessionsScreen(
|
|||||||
title = stringResource(R.string.history_empty_title),
|
title = stringResource(R.string.history_empty_title),
|
||||||
subtitle = stringResource(R.string.history_empty_desc),
|
subtitle = stringResource(R.string.history_empty_desc),
|
||||||
icon = Icons.Filled.History,
|
icon = Icons.Filled.History,
|
||||||
|
actionLabel = stringResource(R.string.empty_action_start_chat),
|
||||||
|
onAction = { NavigationController.navigateTo(ChatScreen) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ fun SkillsScreen(
|
|||||||
isUninstalling = state.isUninstalling,
|
isUninstalling = state.isUninstalling,
|
||||||
uninstallingSkillName = state.uninstallingSkillName,
|
uninstallingSkillName = state.uninstallingSkillName,
|
||||||
onRefresh = viewModel::loadSkills,
|
onRefresh = viewModel::loadSkills,
|
||||||
|
onBrowseHub = { viewModel.setViewMode(SkillsViewMode.HUB) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +220,7 @@ private fun InstalledSkillsView(
|
|||||||
isUninstalling: Boolean,
|
isUninstalling: Boolean,
|
||||||
uninstallingSkillName: String?,
|
uninstallingSkillName: String?,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
|
onBrowseHub: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var showDetail by remember { mutableStateOf<Skill?>(null) }
|
var showDetail by remember { mutableStateOf<Skill?>(null) }
|
||||||
|
|
||||||
@@ -332,6 +334,8 @@ private fun InstalledSkillsView(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
icon = Icons.Filled.Extension,
|
icon = Icons.Filled.Extension,
|
||||||
title = stringResource(R.string.skills_empty_message),
|
title = stringResource(R.string.skills_empty_message),
|
||||||
|
actionLabel = stringResource(R.string.empty_action_browse_hub),
|
||||||
|
onAction = onBrowseHub,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ internal fun HubBrowseView(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
icon = Icons.Filled.Extension,
|
icon = Icons.Filled.Extension,
|
||||||
title = stringResource(R.string.skills_hub_empty_results),
|
title = stringResource(R.string.skills_hub_empty_results),
|
||||||
|
actionLabel = stringResource(R.string.empty_action_retry_search),
|
||||||
|
onAction = { onSearch(hubQuery) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ fun WebhooksScreen(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
title =
|
title =
|
||||||
if (state.subscriptions.isEmpty()) {
|
if (state.subscriptions.isEmpty()) {
|
||||||
"No subscriptions"
|
stringResource(R.string.webhooks_empty_title)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.webhooks_no_matching)
|
stringResource(R.string.webhooks_no_matching)
|
||||||
},
|
},
|
||||||
@@ -351,6 +351,18 @@ fun WebhooksScreen(
|
|||||||
} else {
|
} else {
|
||||||
stringResource(R.string.webhooks_no_matching_desc)
|
stringResource(R.string.webhooks_no_matching_desc)
|
||||||
},
|
},
|
||||||
|
actionLabel =
|
||||||
|
if (state.subscriptions.isEmpty()) {
|
||||||
|
stringResource(R.string.webhooks_action_add)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onAction =
|
||||||
|
if (state.subscriptions.isEmpty()) {
|
||||||
|
{ viewModel.showCreateDialog() }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,6 +36,19 @@
|
|||||||
<string name="content_desc_open_drawer">Open navigation drawer</string>
|
<string name="content_desc_open_drawer">Open navigation drawer</string>
|
||||||
<string name="content_desc_refresh">Refresh</string>
|
<string name="content_desc_refresh">Refresh</string>
|
||||||
<string name="common_save">Save</string>
|
<string name="common_save">Save</string>
|
||||||
|
|
||||||
|
<!-- Empty-state CTAs (issue #618) -->
|
||||||
|
<string name="empty_action_browse_hub">Browse skill hub</string>
|
||||||
|
<string name="empty_action_retry_search">Retry search</string>
|
||||||
|
<string name="empty_action_create_job">Create your first job</string>
|
||||||
|
<string name="empty_action_add_server">Add a server</string>
|
||||||
|
<string name="empty_action_create_subscription">Create subscription</string>
|
||||||
|
<string name="empty_action_connect_telegram">Connect a Telegram channel</string>
|
||||||
|
<string name="empty_action_configure_model">Configure model</string>
|
||||||
|
<string name="empty_action_start_chat">Start a new chat</string>
|
||||||
|
<string name="empty_action_install_plugin">Install from URL</string>
|
||||||
|
<string name="empty_action_explore">Explore Skills or Cron jobs</string>
|
||||||
|
<string name="empty_action_clear_search">Clear search</string>
|
||||||
<string name="common_cancel">Cancel</string>
|
<string name="common_cancel">Cancel</string>
|
||||||
<string name="content_desc_clear_search">Clear search</string>
|
<string name="content_desc_clear_search">Clear search</string>
|
||||||
|
|
||||||
@@ -487,6 +500,7 @@
|
|||||||
<string name="webhooks_sec_status">Global Webhooks Status</string>
|
<string name="webhooks_sec_status">Global Webhooks Status</string>
|
||||||
<string name="webhooks_sec_subscriptions">Subscriptions</string>
|
<string name="webhooks_sec_subscriptions">Subscriptions</string>
|
||||||
<string name="webhooks_empty_desc">No webhook subscriptions configured.</string>
|
<string name="webhooks_empty_desc">No webhook subscriptions configured.</string>
|
||||||
|
<string name="webhooks_empty_title">No subscriptions</string>
|
||||||
<string name="webhooks_search_placeholder">Search subscriptions…</string>
|
<string name="webhooks_search_placeholder">Search subscriptions…</string>
|
||||||
<string name="webhooks_action_add">Add subscription</string>
|
<string name="webhooks_action_add">Add subscription</string>
|
||||||
<string name="webhooks_action_create">Create</string>
|
<string name="webhooks_action_create">Create</string>
|
||||||
@@ -645,6 +659,8 @@
|
|||||||
<string name="mcp_servers_action_test">Test</string>
|
<string name="mcp_servers_action_test">Test</string>
|
||||||
<string name="mcp_servers_add_server">Add server</string>
|
<string name="mcp_servers_add_server">Add server</string>
|
||||||
<string name="mcp_servers_add_http">HTTP URL</string>
|
<string name="mcp_servers_add_http">HTTP URL</string>
|
||||||
|
<string name="mcp_servers_empty_title">No MCP servers</string>
|
||||||
|
<string name="mcp_servers_empty_desc">Add a server above or from the catalog.</string>
|
||||||
<string name="mcp_servers_add_stdio">Stdio command</string>
|
<string name="mcp_servers_add_stdio">Stdio command</string>
|
||||||
<string name="mcp_servers_field_name">Server name</string>
|
<string name="mcp_servers_field_name">Server name</string>
|
||||||
<string name="mcp_servers_field_url">URL (e.g. http://localhost:8000/sse)</string>
|
<string name="mcp_servers_field_url">URL (e.g. http://localhost:8000/sse)</string>
|
||||||
@@ -747,6 +763,8 @@
|
|||||||
<string name="keys_label_not_configured">Not configured</string>
|
<string name="keys_label_not_configured">Not configured</string>
|
||||||
<string name="keys_action_toggle_visibility">Toggle Visibility</string>
|
<string name="keys_action_toggle_visibility">Toggle Visibility</string>
|
||||||
<string name="keys_search_placeholder">Search keys…</string>
|
<string name="keys_search_placeholder">Search keys…</string>
|
||||||
|
<string name="keys_no_matching_title">No matching keys</string>
|
||||||
|
<string name="keys_no_matching_desc">Try a different search term.</string>
|
||||||
<string name="keys_add_header">Add new key</string>
|
<string name="keys_add_header">Add new key</string>
|
||||||
<string name="keys_add_key_label">Key</string>
|
<string name="keys_add_key_label">Key</string>
|
||||||
<string name="keys_add_value_label">Value</string>
|
<string name="keys_add_value_label">Value</string>
|
||||||
|
|||||||
Reference in new issue
Block a user