use axum::{ extract::{Path, Query, State}, response::Html, routing::get, Router, }; use diesel::prelude::*; use serde::Deserialize; use std::sync::Arc; use uuid::Uuid; use crate::core::bot::get_default_bot; use crate::core::shared::schema::workspaces::{workspace_members, workspace_pages, workspaces as workspaces_table}; use crate::core::shared::state::AppState; use super::{DbWorkspace, DbWorkspaceMember, DbWorkspacePage}; #[derive(Debug, Deserialize)] pub struct ListQuery { pub search: Option, } #[derive(Debug, Deserialize)] pub struct PageListQuery { pub parent_id: Option, } fn get_bot_context(state: &AppState) -> (Uuid, Uuid) { let Ok(mut conn) = state.conn.get() else { return (Uuid::nil(), Uuid::nil()); }; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); (org_id, bot_id) } fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } fn render_empty_state(icon: &str, title: &str, description: &str) -> String { format!( r##"
{icon}

{title}

{description}

"## ) } fn render_workspace_card(workspace: &DbWorkspace, member_count: i64, page_count: i64) -> String { let name = html_escape(&workspace.name); let description = workspace .description .as_deref() .map(html_escape) .unwrap_or_else(|| "No description".to_string()); let updated = workspace.updated_at.format("%Y-%m-%d %H:%M").to_string(); let id = workspace.id; let icon = workspace .icon_value .as_deref() .unwrap_or("📁"); format!( r##"
{icon}

{name}

{description}

{member_count} members {page_count} pages {updated}
"## ) } fn render_workspace_row(workspace: &DbWorkspace, member_count: i64, page_count: i64) -> String { let name = html_escape(&workspace.name); let description = workspace .description .as_deref() .map(html_escape) .unwrap_or_else(|| "-".to_string()); let updated = workspace.updated_at.format("%Y-%m-%d %H:%M").to_string(); let id = workspace.id; let icon = workspace.icon_value.as_deref().unwrap_or("📁"); format!( r##" {icon} {name} {description} {member_count} {page_count} {updated} "## ) } fn render_page_item(page: &DbWorkspacePage, child_count: i64) -> String { let title = html_escape(&page.title); let id = page.id; let workspace_id = page.workspace_id; let icon = page.icon_value.as_deref().unwrap_or("📄"); let updated = page.updated_at.format("%Y-%m-%d %H:%M").to_string(); let has_children = if child_count > 0 { format!( r##""## ) } else { r##""##.to_string() }; format!( r##"
{has_children} {icon} {title} {updated}
"## ) } pub async fn workspace_list( State(state): State>, Query(query): Query, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let (org_id, bot_id) = get_bot_context(&state); let mut q = workspaces_table::table .filter(workspaces_table::org_id.eq(org_id)) .filter(workspaces_table::bot_id.eq(bot_id)) .into_boxed(); if let Some(search) = &query.search { let pattern = format!("%{search}%"); q = q.filter( workspaces_table::name .ilike(pattern.clone()) .or(workspaces_table::description.ilike(pattern)), ); } let db_workspaces: Vec = match q .order(workspaces_table::updated_at.desc()) .limit(50) .load(&mut conn) { Ok(w) => w, Err(_) => { return Html(render_empty_state("⚠️", "Error", "Failed to load workspaces")); } }; if db_workspaces.is_empty() { return Html(render_empty_state( "📁", "No Workspaces", "Create your first workspace to get started", )); } let mut rows = String::new(); for workspace in &db_workspaces { let member_count: i64 = workspace_members::table .filter(workspace_members::workspace_id.eq(workspace.id)) .count() .get_result(&mut conn) .unwrap_or(0); let page_count: i64 = workspace_pages::table .filter(workspace_pages::workspace_id.eq(workspace.id)) .count() .get_result(&mut conn) .unwrap_or(0); rows.push_str(&render_workspace_row(workspace, member_count, page_count)); } Html(format!( r##"{rows}
Name Description Members Pages Updated Actions
"## )) } pub async fn workspace_cards( State(state): State>, Query(query): Query, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let (org_id, bot_id) = get_bot_context(&state); let mut q = workspaces_table::table .filter(workspaces_table::org_id.eq(org_id)) .filter(workspaces_table::bot_id.eq(bot_id)) .into_boxed(); if let Some(search) = &query.search { let pattern = format!("%{search}%"); q = q.filter( workspaces_table::name .ilike(pattern.clone()) .or(workspaces_table::description.ilike(pattern)), ); } let db_workspaces: Vec = match q .order(workspaces_table::updated_at.desc()) .limit(50) .load(&mut conn) { Ok(w) => w, Err(_) => { return Html(render_empty_state("⚠️", "Error", "Failed to load workspaces")); } }; if db_workspaces.is_empty() { return Html(render_empty_state( "📁", "No Workspaces", "Create your first workspace to get started", )); } let mut cards = String::new(); for workspace in &db_workspaces { let member_count: i64 = workspace_members::table .filter(workspace_members::workspace_id.eq(workspace.id)) .count() .get_result(&mut conn) .unwrap_or(0); let page_count: i64 = workspace_pages::table .filter(workspace_pages::workspace_id.eq(workspace.id)) .count() .get_result(&mut conn) .unwrap_or(0); cards.push_str(&render_workspace_card(workspace, member_count, page_count)); } Html(format!(r##"
{cards}
"##)) } pub async fn workspace_count(State(state): State>) -> Html { let Ok(mut conn) = state.conn.get() else { return Html("0".to_string()); }; let (org_id, bot_id) = get_bot_context(&state); let count: i64 = workspaces_table::table .filter(workspaces_table::org_id.eq(org_id)) .filter(workspaces_table::bot_id.eq(bot_id)) .count() .get_result(&mut conn) .unwrap_or(0); Html(count.to_string()) } pub async fn workspace_detail( State(state): State>, Path(workspace_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let workspace: DbWorkspace = match workspaces_table::table .filter(workspaces_table::id.eq(workspace_id)) .first(&mut conn) { Ok(w) => w, Err(_) => { return Html(render_empty_state("❌", "Not Found", "Workspace not found")); } }; let member_count: i64 = workspace_members::table .filter(workspace_members::workspace_id.eq(workspace_id)) .count() .get_result(&mut conn) .unwrap_or(0); let page_count: i64 = workspace_pages::table .filter(workspace_pages::workspace_id.eq(workspace_id)) .count() .get_result(&mut conn) .unwrap_or(0); let name = html_escape(&workspace.name); let description = workspace .description .as_deref() .map(html_escape) .unwrap_or_else(|| "No description".to_string()); let icon = workspace.icon_value.as_deref().unwrap_or("📁"); let created = workspace.created_at.format("%Y-%m-%d %H:%M").to_string(); let updated = workspace.updated_at.format("%Y-%m-%d %H:%M").to_string(); Html(format!( r##"
{icon}

{name}

{description}

Members {member_count}
Pages {page_count}
Created: {created} Updated: {updated}
"## )) } pub async fn workspace_pages( State(state): State>, Path(workspace_id): Path, Query(query): Query, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let pages: Vec = match query.parent_id { Some(parent_id) => workspace_pages::table .filter(workspace_pages::workspace_id.eq(workspace_id)) .filter(workspace_pages::parent_id.eq(parent_id)) .order(workspace_pages::position.asc()) .load(&mut conn) .unwrap_or_default(), None => workspace_pages::table .filter(workspace_pages::workspace_id.eq(workspace_id)) .filter(workspace_pages::parent_id.is_null()) .order(workspace_pages::position.asc()) .load(&mut conn) .unwrap_or_default(), }; if pages.is_empty() && query.parent_id.is_none() { return Html(render_empty_state( "📄", "No Pages", "Create your first page to get started", )); } let mut items = String::new(); for page in &pages { let child_count: i64 = workspace_pages::table .filter(workspace_pages::parent_id.eq(page.id)) .count() .get_result(&mut conn) .unwrap_or(0); items.push_str(&render_page_item(page, child_count)); } if query.parent_id.is_some() { Html(items) } else { Html(format!( r##"

Pages

{items}
"## )) } } pub async fn workspace_members( State(state): State>, Path(workspace_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let members: Vec = workspace_members::table .filter(workspace_members::workspace_id.eq(workspace_id)) .order(workspace_members::joined_at.asc()) .load(&mut conn) .unwrap_or_default(); if members.is_empty() { return Html(render_empty_state( "👥", "No Members", "This workspace has no members", )); } let mut rows = String::new(); for member in &members { let user_id = member.user_id; let role = html_escape(&member.role); let joined = member.joined_at.format("%Y-%m-%d").to_string(); let role_class = match role.as_str() { "owner" => "badge-primary", "admin" => "badge-warning", "editor" => "badge-info", _ => "badge-secondary", }; rows.push_str(&format!( r##" {user_id} {role} {joined} "## )); } Html(format!( r##"

Members

{rows}
User Role Joined Actions
"## )) } pub async fn page_detail( State(state): State>, Path(page_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let page: DbWorkspacePage = match workspace_pages::table .filter(workspace_pages::id.eq(page_id)) .first(&mut conn) { Ok(p) => p, Err(_) => { return Html(render_empty_state("❌", "Not Found", "Page not found")); } }; let title = html_escape(&page.title); let icon = page.icon_value.as_deref().unwrap_or("📄"); let created = page.created_at.format("%Y-%m-%d %H:%M").to_string(); let updated = page.updated_at.format("%Y-%m-%d %H:%M").to_string(); let workspace_id = page.workspace_id; let content_preview = if page.content.is_null() || page.content == serde_json::json!([]) { r##"

This page is empty. Click Edit to add content.

"##.to_string() } else { r##"
"##.to_string().replace("{page_id}", &page_id.to_string()) }; Html(format!( r##"
Created: {created} Updated: {updated}
{content_preview}
"## )) } pub async fn new_workspace_form(State(_state): State>) -> Html { Html( r##"
"## .to_string(), ) } pub async fn new_page_form( State(_state): State>, Path(workspace_id): Path, Query(query): Query, ) -> Html { let parent_input = match query.parent_id { Some(parent_id) => format!(r##""##), None => String::new(), }; Html(format!( r##"
{parent_input}
"## )) } pub async fn workspace_settings( State(state): State>, Path(workspace_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let workspace: DbWorkspace = match workspaces_table::table .filter(workspaces_table::id.eq(workspace_id)) .first(&mut conn) { Ok(w) => w, Err(_) => { return Html(render_empty_state("❌", "Not Found", "Workspace not found")); } }; let name = html_escape(&workspace.name); let description = workspace.description.as_deref().map(html_escape).unwrap_or_default(); Html(format!( r##"
"## )) } pub async fn add_member_form( State(_state): State>, Path(workspace_id): Path, ) -> Html { Html(format!( r##"
"## )) } pub async fn search_results( State(state): State>, Path(workspace_id): Path, Query(query): Query, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let search_term = match &query.search { Some(s) if !s.is_empty() => s, _ => { return Html(render_empty_state("🔍", "Search", "Enter a search term")); } }; let pattern = format!("%{search_term}%"); let pages: Vec = workspace_pages::table .filter(workspace_pages::workspace_id.eq(workspace_id)) .filter(workspace_pages::title.ilike(&pattern)) .order(workspace_pages::updated_at.desc()) .limit(20) .load(&mut conn) .unwrap_or_default(); if pages.is_empty() { return Html(render_empty_state( "🔍", "No Results", "No pages match your search", )); } let mut items = String::new(); for page in &pages { let title = html_escape(&page.title); let id = page.id; let icon = page.icon_value.as_deref().unwrap_or("📄"); let updated = page.updated_at.format("%Y-%m-%d %H:%M").to_string(); items.push_str(&format!( r##"
{icon} {title} {updated}
"## )); } Html(format!( r##"

Search Results ({count})

{items}
"##, count = pages.len() )) } pub fn configure_workspaces_ui_routes() -> Router> { Router::new() .route("/api/ui/workspaces", get(workspace_list)) .route("/api/ui/workspaces/cards", get(workspace_cards)) .route("/api/ui/workspaces/count", get(workspace_count)) .route("/api/ui/workspaces/new", get(new_workspace_form)) .route("/api/ui/workspaces/{workspace_id}", get(workspace_detail)) .route("/api/ui/workspaces/{workspace_id}/pages", get(workspace_pages)) .route("/api/ui/workspaces/{workspace_id}/pages/new", get(new_page_form)) .route("/api/ui/workspaces/{workspace_id}/members", get(workspace_members)) .route("/api/ui/workspaces/{workspace_id}/members/add", get(add_member_form)) .route("/api/ui/workspaces/{workspace_id}/settings", get(workspace_settings)) .route("/api/ui/workspaces/{workspace_id}/search", get(search_results)) .route("/api/ui/pages/{page_id}", get(page_detail)) }