From 307375a348b828e66887641aabfbd784bd12bc55 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 9 Mar 2026 22:01:34 +0300 Subject: [PATCH] pinakes-ui: add plugin action executor Signed-off-by: NotAShelf Change-Id: Ic6dc2c6e3ef58dacad4829037226b0cf6a6a6964 --- crates/pinakes-ui/src/plugin_ui/actions.rs | 138 +++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 crates/pinakes-ui/src/plugin_ui/actions.rs diff --git a/crates/pinakes-ui/src/plugin_ui/actions.rs b/crates/pinakes-ui/src/plugin_ui/actions.rs new file mode 100644 index 0000000..6da8aa2 --- /dev/null +++ b/crates/pinakes-ui/src/plugin_ui/actions.rs @@ -0,0 +1,138 @@ +//! Action execution system for plugin UI pages +//! +//! This module provides the action execution system that handles +//! user interactions with plugin UI elements. + +use pinakes_plugin_api::{ActionDefinition, ActionRef, HttpMethod}; + +use crate::client::ApiClient; + +/// Result of an action execution +#[derive(Debug, Clone)] +pub enum ActionResult { + /// Action completed successfully + Success(serde_json::Value), + /// Action failed + Error(String), + /// Navigation action + Navigate(String), + /// No meaningful result (e.g. 204 No Content) + None, +} + +/// Execute an action defined in the UI schema +pub async fn execute_action( + client: &ApiClient, + action_ref: &ActionRef, + form_data: Option<&serde_json::Value>, +) -> Result { + match action_ref { + ActionRef::Name(name) => { + tracing::warn!("Named action '{}' not implemented yet", name); + Ok(ActionResult::None) + }, + ActionRef::Inline(action) => { + execute_inline_action(client, action, form_data).await + }, + } +} + +/// Execute an inline action definition +async fn execute_inline_action( + client: &ApiClient, + action: &ActionDefinition, + form_data: Option<&serde_json::Value>, +) -> Result { + // Build URL from path + let url = action.path.clone(); + + // Merge action params with form data into query string for GET, body for + // others + let method = match action.method { + HttpMethod::Get => reqwest::Method::GET, + HttpMethod::Post => reqwest::Method::POST, + HttpMethod::Put => reqwest::Method::PUT, + HttpMethod::Patch => reqwest::Method::PATCH, + HttpMethod::Delete => reqwest::Method::DELETE, + }; + + let mut request = client.raw_request(method.clone(), &url); + + // For GET, merge params into query string; for mutating methods, send as + // JSON body + if method == reqwest::Method::GET { + let query_pairs: Vec<(String, String)> = action + .params + .iter() + .map(|(k, v)| { + let val = match v { + serde_json::Value::String(s) => s.clone(), + other => other.to_string(), + }; + (k.clone(), val) + }) + .collect(); + if !query_pairs.is_empty() { + request = request.query(&query_pairs); + } + } else { + // Build body: merge action.params with form_data + let mut merged: serde_json::Map = action + .params + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + if let Some(fd) = form_data { + if let Some(obj) = fd.as_object() { + for (k, v) in obj { + merged.insert(k.clone(), v.clone()); + } + } + } + if !merged.is_empty() { + request = request.json(&merged); + } + } + + let response = request.send().await.map_err(|e| e.to_string())?; + let status = response.status(); + + if !status.is_success() { + let error_text = response.text().await.unwrap_or_default(); + return Ok(ActionResult::Error(format!( + "Action failed: {} - {}", + status.as_u16(), + error_text + ))); + } + + if status.as_u16() == 204 { + // Navigate on success if configured + if let Some(route) = &action.navigate_to { + return Ok(ActionResult::Navigate(route.clone())); + } + return Ok(ActionResult::None); + } + + let value: serde_json::Value = + response.json().await.map_err(|e| e.to_string())?; + + if let Some(route) = &action.navigate_to { + return Ok(ActionResult::Navigate(route.clone())); + } + + Ok(ActionResult::Success(value)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_action_result_variants() { + let _ = ActionResult::None; + let _ = ActionResult::Success(serde_json::json!({"ok": true})); + let _ = ActionResult::Error("error".to_string()); + let _ = ActionResult::Navigate("/page".to_string()); + } +}