use serde::Deserialize;
use crate::components::SwapKey;
use crate::http::{ModalGet, RouteQueryBuilder, RouteUrl};
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ModalFormQuery {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub refresh: Option<String>,
#[serde(default)]
pub target_input: Option<String>,
}
impl ModalFormQuery {
pub fn form_name(&self) -> String {
self.name.clone().unwrap_or_default()
}
pub fn refresh_table(&self) -> String {
self.refresh.clone().unwrap_or_default()
}
pub fn target_input(&self) -> String {
self.target_input.clone().unwrap_or_default()
}
pub fn refreshes_table<T: SwapKey>(&self) -> bool {
self.refresh.as_deref() == Some(T::ID)
}
}
pub trait CreateModal: SwapKey {
type Get: ModalGet + RouteUrl + Copy + Default;
type Post: RouteUrl + Copy + Default;
const FORM_NAME: &'static str;
}
#[macro_export]
macro_rules! impl_create_modal {
($modal:ty, $get:ty, $post:ty, $form:expr) => {
impl $crate::web::CreateModal for $modal {
type Get = $get;
type Post = $post;
const FORM_NAME: &'static str = $form;
}
};
}
pub fn modal_create_href_for_table<T: SwapKey>(href: &str, form_name: &str) -> String {
let mut out = href.to_string();
append_query_if_absent(&mut out, "name", form_name);
append_query_if_absent(&mut out, "refresh", T::ID);
out
}
pub fn modal_create_href_for_picker(href: &str, form_name: &str, target_input: &str) -> String {
let mut out = href.to_string();
append_query_if_absent(&mut out, "name", form_name);
append_query_if_absent(&mut out, "target_input", target_input);
out
}
fn query_has_param(url: &str, key: &str) -> bool {
url.contains(&format!("?{key}=")) || url.contains(&format!("&{key}="))
}
fn append_query_if_absent(url: &mut String, key: &str, value: &str) {
if value.is_empty() || query_has_param(url, key) {
return;
}
let sep = if url.contains('?') { '&' } else { '?' };
url.push(sep);
url.push_str(key);
url.push('=');
url.push_str(value);
}
pub fn modal_create_get_url<T: SwapKey>(route: impl RouteUrl, form_name: &str) -> String {
modal_create_url(route, form_name, T::ID, "", true)
}
pub fn modal_create_get_for<M: CreateModal, T: SwapKey>() -> String {
modal_create_get_url::<T>(M::Get::default(), M::FORM_NAME)
}
pub fn modal_create_get_for_picker<M: CreateModal>(target_input: &str) -> String {
modal_create_url(M::Get::default(), M::FORM_NAME, "", target_input, true)
}
pub fn modal_create_post_url(route: impl RouteUrl, form_name: &str, refresh: &str) -> String {
modal_create_post_query(route, form_name, refresh, "")
}
pub fn modal_create_post_query(
route: impl RouteUrl,
form_name: &str,
refresh: &str,
target_input: &str,
) -> String {
modal_create_url(route, form_name, refresh, target_input, false)
}
pub fn modal_create_post_url_for_table<T: SwapKey>(
route: impl RouteUrl,
form_name: &str,
) -> String {
modal_create_post_url(route, form_name, T::ID)
}
pub fn modal_create_post_for<M: CreateModal, T: SwapKey>() -> String {
modal_create_post_url_for_table::<T>(M::Post::default(), M::FORM_NAME)
}
pub fn modal_edit_post_url(route: impl RouteUrl, form_name: &str) -> String {
modal_create_url(route, form_name, "", "", false)
}
fn modal_create_url(
route: impl RouteUrl,
form_name: &str,
refresh: &str,
target_input: &str,
trailing_slash: bool,
) -> String {
let mut builder = RouteQueryBuilder::new(route);
if !form_name.is_empty() {
builder = builder.query("name", form_name);
}
if !refresh.is_empty() {
builder = builder.query("refresh", refresh);
}
if !target_input.is_empty() {
builder = builder.query("target_input", target_input);
}
if trailing_slash {
builder.build()
} else {
builder.build_with_query()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::swap::SwapKey;
use crate::http::{ModalGet, RouteTag, RouteUrl};
use crate::swap_key;
swap_key!(TestCreateModalKey, "test-create-modal");
swap_key!(TestTableKey, "test-table");
#[derive(Clone, Copy, Default)]
pub struct TestCreateGetRoute;
impl RouteTag for TestCreateGetRoute {
const PATH: &'static str = "/test/create";
}
impl RouteUrl for TestCreateGetRoute {
fn path(self) -> String {
Self::PATH.to_owned()
}
fn url(self) -> String {
Self::PATH.to_owned()
}
}
impl ModalGet for TestCreateGetRoute {}
#[derive(Clone, Copy, Default)]
pub struct TestCreatePostRoute;
impl RouteTag for TestCreatePostRoute {
const PATH: &'static str = "/test/create";
}
impl RouteUrl for TestCreatePostRoute {
fn path(self) -> String {
Self::PATH.to_owned()
}
fn url(self) -> String {
Self::PATH.to_owned()
}
}
impl CreateModal for TestCreateModalKey {
type Get = TestCreateGetRoute;
type Post = TestCreatePostRoute;
const FORM_NAME: &'static str = "p_test.CreateForm";
}
#[test]
fn modal_create_urls_embed_table_refresh() {
let get = modal_create_get_for::<TestCreateModalKey, TestTableKey>();
assert!(get.contains("/test/create/?"), "{get}");
assert!(get.contains("name=p_test.CreateForm"), "{get}");
assert!(get.contains("refresh=test-table"), "{get}");
let post = modal_create_post_for::<TestCreateModalKey, TestTableKey>();
assert!(post.contains("/test/create?"), "{post}");
assert!(post.contains("refresh=test-table"), "{post}");
assert!(!post.contains("/test/create/?"), "{post}");
}
#[test]
fn modal_create_href_preserves_existing_query() {
let href = modal_create_href_for_table::<TestTableKey>(
"/test/create/?ParentID=9",
"p_test.CreateForm",
);
assert!(href.contains("ParentID=9"), "{href}");
assert!(href.contains("name=p_test.CreateForm"), "{href}");
assert!(href.contains("refresh=test-table"), "{href}");
assert_eq!(href.matches('?').count(), 1, "{href}");
}
#[test]
fn modal_create_get_for_picker_embeds_target_input() {
let get = modal_create_get_for_picker::<TestCreateModalKey>("CustomerID");
assert!(get.contains("/test/create/?"), "{get}");
assert!(get.contains("name=p_test.CreateForm"), "{get}");
assert!(get.contains("target_input=CustomerID"), "{get}");
assert!(!get.contains("refresh="), "{get}");
}
#[test]
fn modal_create_href_for_picker_preserves_existing_query() {
let href = modal_create_href_for_picker(
"/test/create/?ParentID=9",
"p_test.CreateForm",
"ParentID",
);
assert!(href.contains("ParentID=9"), "{href}");
assert!(href.contains("name=p_test.CreateForm"), "{href}");
assert!(href.contains("target_input=ParentID"), "{href}");
assert!(!href.contains("refresh="), "{href}");
}
#[test]
fn modal_create_post_query_embeds_target_input() {
let post =
modal_create_post_query(TestCreatePostRoute, "p_test.CreateForm", "", "CustomerID");
assert!(post.contains("target_input=CustomerID"), "{post}");
assert!(!post.contains("refresh="), "{post}");
}
#[test]
fn modal_form_query_matches_table_key() {
let q = ModalFormQuery {
refresh: Some(TestTableKey::ID.to_string()),
..Default::default()
};
assert!(q.refreshes_table::<TestTableKey>());
assert!(!q.refreshes_table::<TestCreateModalKey>());
}
}
#[cfg(all(test, feature = "plugin-crm"))]
mod crm_button_tests {
use crate::components::table_create_button;
use crate::plugins::crm::keys::{CompanyCreateModalKey, CompanyTableKey};
#[test]
fn company_table_create_button_hx_get() {
let html = table_create_button::<CompanyTableKey, CompanyCreateModalKey>(
Some("plus"),
"btn-square btn-outline btn-sm",
)
.into_string();
assert!(html.contains("hx-get="), "{html}");
assert!(
html.contains("/crm/companies/create/?name=p_crm.CompanyCreateForm"),
"{html}"
);
assert!(html.contains("refresh=crm-company-table"), "{html}");
assert_eq!(
html.matches("name=p_crm.CompanyCreateForm").count(),
1,
"{html}"
);
}
}