#![allow(clippy::doc_markdown)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordActionRepresentation {
pub actions: HashMap<String, Vec<ActionRepresentation>>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionRepresentation {
pub api_name: String,
pub label: String,
pub action_type: String,
pub is_massable: bool,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
impl<A: crate::auth::Authenticator> crate::api::ui::UiHandler<A> {
pub async fn record_actions(
&self,
ids: &[&str],
) -> crate::error::Result<RecordActionRepresentation> {
let capacity = 15 + ids.iter().map(|s| s.len() + 1).sum::<usize>();
let mut path = String::with_capacity(capacity);
path.push_str("actions/record/");
for (i, id) in ids.iter().enumerate() {
if i > 0 {
path.push(',');
}
path.push_str(id);
}
self.get(&path, None, "Failed to fetch record actions")
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::builder;
use crate::test_support::{MockAuthenticator, Must};
use serde_json::json;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
const VALID_ID: &str = "001000000000001AAA";
const VALID_ID2: &str = "001000000000002AAA";
async fn make_client(server: &MockServer) -> crate::client::ForceClient<MockAuthenticator> {
let auth = MockAuthenticator::new("test_token", &server.uri());
builder().authenticate(auth).build().await.must()
}
fn action_json(api_name: &str) -> serde_json::Value {
json!({
"apiName": api_name,
"label": api_name,
"actionType": "StandardButton",
"isMassable": false
})
}
#[tokio::test]
async fn test_record_actions_single_id_success() {
let server = MockServer::start().await;
let client = make_client(&server).await;
let response_body = json!({
"actions": {
VALID_ID: [
action_json("Edit"),
action_json("Delete")
]
}
});
Mock::given(method("GET"))
.and(path(format!(
"/services/data/v60.0/ui-api/actions/record/{VALID_ID}"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
.expect(1)
.mount(&server)
.await;
let result = client.ui().record_actions(&[VALID_ID]).await.must();
assert!(result.actions.contains_key(VALID_ID));
let actions = &result.actions[VALID_ID];
assert_eq!(actions.len(), 2);
assert_eq!(actions[0].api_name, "Edit");
assert_eq!(actions[1].api_name, "Delete");
}
#[tokio::test]
async fn test_record_actions_multiple_ids_success() {
let server = MockServer::start().await;
let client = make_client(&server).await;
let joined = format!("{VALID_ID},{VALID_ID2}");
let response_body = json!({
"actions": {
VALID_ID: [action_json("Edit")],
VALID_ID2: [action_json("Delete")]
}
});
Mock::given(method("GET"))
.and(path(format!(
"/services/data/v60.0/ui-api/actions/record/{joined}"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
.expect(1)
.mount(&server)
.await;
let result = client
.ui()
.record_actions(&[VALID_ID, VALID_ID2])
.await
.must();
assert_eq!(result.actions.len(), 2);
assert!(result.actions.contains_key(VALID_ID));
assert!(result.actions.contains_key(VALID_ID2));
}
#[tokio::test]
async fn test_record_actions_not_found() {
let server = MockServer::start().await;
let client = make_client(&server).await;
Mock::given(method("GET"))
.and(path(format!(
"/services/data/v60.0/ui-api/actions/record/{VALID_ID}"
)))
.respond_with(ResponseTemplate::new(404).set_body_json(json!([{
"errorCode": "NOT_FOUND",
"message": "Record not found"
}])))
.expect(1)
.mount(&server)
.await;
let result = client.ui().record_actions(&[VALID_ID]).await;
let Err(err) = result else {
panic!("Expected an error");
};
assert!(
matches!(
err,
crate::error::ForceError::Api(_) | crate::error::ForceError::Http(_)
),
"Expected Api or Http error, got: {err}"
);
}
#[test]
fn test_record_action_representation_deserialize() {
let json_str = r#"{
"actions": {
"001000000000001AAA": [
{
"apiName": "NewTask",
"label": "New Task",
"actionType": "QuickAction",
"isMassable": true
}
]
}
}"#;
let rep: RecordActionRepresentation = serde_json::from_str(json_str).must();
let actions = rep.actions.get("001000000000001AAA").must();
assert_eq!(actions.len(), 1);
assert_eq!(actions[0].api_name, "NewTask");
assert_eq!(actions[0].action_type, "QuickAction");
assert!(actions[0].is_massable);
}
}