feat: implement configurable columns for item list page

Co-authored-by: aider (openai/andrew/openrouter/deepseek/deepseek-chat-v3.1) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-28 15:15:38 -03:00
parent 888e6457dd
commit 761542743c

View File

@@ -7,6 +7,8 @@ use axum::{
}; };
use rusqlite::Connection; use rusqlite::Connection;
use serde::Deserialize; use serde::Deserialize;
use crate::config::ColumnConfig;
use std::collections::HashMap;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ListQueryParams { pub struct ListQueryParams {
@@ -39,8 +41,9 @@ async fn list_items(
Query(params): Query<ListQueryParams>, Query(params): Query<ListQueryParams>,
) -> Result<Html<String>, Html<String>> { ) -> Result<Html<String>, Html<String>> {
let conn = state.db.lock().await; let conn = state.db.lock().await;
let settings = &state.settings;
let result = build_item_list(&conn, &params); let result = build_item_list(&conn, &params, &settings.list_format);
match result { match result {
Ok(html) => Ok(Html(html)), Ok(html) => Ok(Html(html)),
@@ -48,7 +51,7 @@ async fn list_items(
} }
} }
fn build_item_list(conn: &Connection, params: &ListQueryParams) -> Result<String> { fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[ColumnConfig]) -> Result<String> {
let tags: Vec<String> = params.tags let tags: Vec<String> = params.tags
.as_ref() .as_ref()
.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()) .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
@@ -77,40 +80,94 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams) -> Result<String
vec![] vec![]
}; };
// Get tags for all items in the page // Get tags and meta for all items in the page
let item_ids: Vec<i64> = page_items.iter().filter_map(|item| item.id).collect(); let item_ids: Vec<i64> = page_items.iter().filter_map(|item| item.id).collect();
let tags_map = db::get_tags_for_items(conn, &item_ids)?; let tags_map = db::get_tags_for_items(conn, &item_ids)?;
let meta_map = db::get_meta_for_items(conn, &item_ids)?;
let mut html = String::new(); let mut html = String::new();
html.push_str("<html><head><title>Items</title></head><body>"); html.push_str("<html><head><title>Items</title>");
html.push_str("<style>");
html.push_str("table { border-collapse: collapse; width: 100%; }");
html.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }");
html.push_str("th { background-color: #f2f2f2; }");
html.push_str("tr:nth-child(even) { background-color: #f9f9f9; }");
html.push_str("</style>");
html.push_str("</head><body>");
html.push_str("<h1>Items</h1>"); html.push_str("<h1>Items</h1>");
html.push_str("<p><a href=\"/swagger\">API Documentation</a></p>"); html.push_str("<p><a href=\"/swagger\">API Documentation</a></p>");
html.push_str("<ul>");
// Start table
html.push_str("<table>");
// Table headers
html.push_str("<tr>");
for column in columns {
html.push_str(&format!("<th>{}</th>", column.label));
}
html.push_str("<th>Actions</th>");
html.push_str("</tr>");
// Table rows
for item in page_items { for item in page_items {
let item_id = item.id.unwrap_or(0); let item_id = item.id.unwrap_or(0);
let tags_html = if let Some(tags) = tags_map.get(&item_id) { let tags = tags_map.get(&item_id).cloned().unwrap_or_default();
let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect(); let meta: HashMap<String, String> = meta_map.get(&item_id)
if tag_names.is_empty() { .map(|metas| metas.iter().map(|m| (m.name.clone(), m.value.clone())).collect())
String::new() .unwrap_or_default();
html.push_str("<tr>");
for column in columns {
let value = match column.name.as_str() {
"id" => item.id.map(|id| id.to_string()).unwrap_or_default(),
"time" => item.ts.format("%Y-%m-%d %H:%M:%S").to_string(),
"size" => item.size.map(|s| s.to_string()).unwrap_or_default(),
"tags" => tags.iter().map(|t| t.name.clone()).collect::<Vec<_>>().join(", "),
_ => {
if column.name.starts_with("meta:") {
let meta_key = &column.name[5..];
meta.get(meta_key).cloned().unwrap_or_default()
} else { } else {
format!(" [{}]", tag_names.join(", ")) String::new()
}
} }
} else {
String::new()
}; };
html.push_str(&format!( // Apply max_len if specified
"<li><a href=\"/item/{}\">Item #{}</a> - {}{} - <a href=\"/api/item/{}/content\">Download</a></li>", let display_value = if let Some(max_len_str) = &column.max_len {
item_id, if let Ok(max_len) = max_len_str.parse::<usize>() {
item_id, if value.chars().count() > max_len {
item.ts.format("%Y-%m-%d %H:%M:%S"), let truncated: String = value.chars().take(max_len).collect();
tags_html, format!("{}...", truncated)
item_id } else {
)); value
}
} else {
value
}
} else {
value
};
// Apply alignment
let align_style = match column.align {
crate::config::ColumnAlignment::Left => "text-align: left;",
crate::config::ColumnAlignment::Right => "text-align: right;",
};
html.push_str(&format!("<td style=\"{}\">{}</td>", align_style, display_value));
} }
html.push_str("</ul>"); // Actions column
html.push_str(&format!(
"<td><a href=\"/item/{}\">View</a> | <a href=\"/api/item/{}/content\">Download</a></td>",
item_id, item_id
));
html.push_str("</tr>");
}
html.push_str("</table>");
// Add pagination info // Add pagination info
html.push_str(&format!("<p>Showing {} to {} of {} items</p>", html.push_str(&format!("<p>Showing {} to {} of {} items</p>",