Fix overlapping route panic: remove duplicate /api/docs/import from drive module

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-11 18:49:04 -03:00
parent 9c2a4dbb97
commit 3fc3c58816
20 changed files with 2355 additions and 2181 deletions

View file

@ -1,21 +1,21 @@
use crate::docs::storage::{
create_new_document, delete_document_from_drive, get_current_user_id,
list_documents_from_drive, load_document_from_drive, save_document_to_drive,
list_documents_from_drive, load_document_from_drive, save_document, save_document_to_drive,
};
use crate::docs::types::{
DocsSaveRequest, DocsSaveResponse, DocsAiRequest, DocsAiResponse, Document, DocumentMetadata,
SearchQuery, TemplateResponse,
};
use crate::docs::utils::{convert_to_html, detect_document_format, html_to_markdown, markdown_to_html, rtf_to_html, strip_html};
use crate::docs::utils::{detect_document_format, html_to_markdown, markdown_to_html, rtf_to_html, strip_html};
use crate::docs::types::{
AcceptRejectAllRequest, AcceptRejectChangeRequest, AddCommentRequest, AddEndnoteRequest,
AddFootnoteRequest, ApplyStyleRequest, CompareDocumentsRequest, CompareDocumentsResponse,
CommentReply, ComparisonSummary, CreateStyleRequest, DeleteCommentRequest, DeleteEndnoteRequest,
DeleteFootnoteRequest, DeleteStyleRequest, DocumentComment, DocumentComparison, DocumentDiff,
DocumentStyle, EnableTrackChangesRequest, Endnote, Footnote, GenerateTocRequest,
EnableTrackChangesRequest, Endnote, Footnote, GenerateTocRequest,
GetOutlineRequest, ListCommentsResponse, ListEndnotesResponse, ListFootnotesResponse,
ListStylesResponse, ListTrackChangesResponse, OutlineItem, OutlineResponse, ReplyCommentRequest,
ResolveCommentRequest, TableOfContents, TocEntry, TocResponse, TrackChange, UpdateEndnoteRequest,
ResolveCommentRequest, TableOfContents, TocEntry, TocResponse, UpdateEndnoteRequest,
UpdateFootnoteRequest, UpdateStyleRequest, UpdateTocRequest,
};
use crate::shared::state::AppState;
@ -25,6 +25,7 @@ use axum::{
response::IntoResponse,
Json,
};
use chrono::Utc;
use docx_rs::{AlignmentType, Docx, Paragraph, Run};
use log::error;
use std::sync::Arc;
@ -601,7 +602,7 @@ pub async fn handle_add_comment(
comments.push(comment.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -650,7 +651,7 @@ pub async fn handle_reply_comment(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -692,7 +693,7 @@ pub async fn handle_resolve_comment(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -728,7 +729,7 @@ pub async fn handle_delete_comment(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -788,7 +789,7 @@ pub async fn handle_enable_track_changes(
doc.track_changes_enabled = req.enabled;
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -829,7 +830,7 @@ pub async fn handle_accept_reject_change(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -867,7 +868,7 @@ pub async fn handle_accept_reject_all(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -929,7 +930,7 @@ pub async fn handle_generate_toc(
let mut entries = Vec::new();
let content = &doc.content;
let mut position = 0;
for level in 1..=req.max_level {
let tag = format!("<h{level}>");
@ -955,7 +956,7 @@ pub async fn handle_generate_toc(
break;
}
}
position = search_pos;
}
entries.sort_by_key(|e| e.position);
@ -972,7 +973,7 @@ pub async fn handle_generate_toc(
doc.toc = Some(toc.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1056,7 +1057,7 @@ pub async fn handle_add_footnote(
footnotes.push(footnote.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1097,7 +1098,7 @@ pub async fn handle_update_footnote(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1136,7 +1137,7 @@ pub async fn handle_delete_footnote(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1206,7 +1207,7 @@ pub async fn handle_add_endnote(
endnotes.push(endnote.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1264,7 +1265,7 @@ pub async fn handle_update_endnote(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1303,7 +1304,7 @@ pub async fn handle_delete_endnote(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1364,7 +1365,7 @@ pub async fn handle_create_style(
styles.push(req.style.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1405,7 +1406,7 @@ pub async fn handle_update_style(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1441,7 +1442,7 @@ pub async fn handle_delete_style(
}
doc.updated_at = Utc::now();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
@ -1620,11 +1621,12 @@ pub async fn handle_import_document(
.to_string();
let user_id = get_current_user_id();
let mut doc = create_new_document(&title);
let mut doc = create_new_document();
doc.title = title;
doc.content = content;
doc.owner_id = user_id.clone();
if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await {
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),

View file

@ -107,6 +107,13 @@ pub async fn load_docx_from_bytes(
updated_at: Utc::now(),
collaborators: Vec::new(),
version: 1,
track_changes: None,
comments: None,
footnotes: None,
endnotes: None,
styles: None,
toc: None,
track_changes_enabled: false,
})
}
@ -188,29 +195,27 @@ pub fn convert_docx_to_html(bytes: &[u8]) -> Result<String, String> {
docx_rs::DocumentChild::Table(table) => {
html.push_str("<table style=\"border-collapse:collapse;width:100%\">");
for row in &table.rows {
if let docx_rs::TableChild::TableRow(tr) = row {
html.push_str("<tr>");
for cell in &tr.cells {
if let docx_rs::TableRowChild::TableCell(tc) = cell {
html.push_str("<td style=\"border:1px solid #ccc;padding:8px\">");
for para in &tc.children {
if let docx_rs::TableCellContent::Paragraph(p) = para {
for content in &p.children {
if let docx_rs::ParagraphChild::Run(run) = content {
for child in &run.children {
if let docx_rs::RunChild::Text(text) = child {
html.push_str(&escape_html(&text.text));
}
}
let docx_rs::TableChild::TableRow(tr) = row;
html.push_str("<tr>");
for cell in &tr.cells {
let docx_rs::TableRowChild::TableCell(tc) = cell;
html.push_str("<td style=\"border:1px solid #ccc;padding:8px\">");
for para in &tc.children {
if let docx_rs::TableCellContent::Paragraph(p) = para {
for content in &p.children {
if let docx_rs::ParagraphChild::Run(run) = content {
for child in &run.children {
if let docx_rs::RunChild::Text(text) = child {
html.push_str(&escape_html(&text.text));
}
}
}
}
html.push_str("</td>");
}
}
html.push_str("</tr>");
html.push_str("</td>");
}
html.push_str("</tr>");
}
html.push_str("</table>");
}
@ -374,6 +379,14 @@ pub async fn save_document_to_drive(
Ok(doc_path)
}
pub async fn save_document(
state: &Arc<AppState>,
user_identifier: &str,
doc: &Document,
) -> Result<String, String> {
save_document_to_drive(state, user_identifier, &doc.id, &doc.title, &doc.content).await
}
pub async fn load_document_from_drive(
state: &Arc<AppState>,
user_identifier: &str,
@ -447,6 +460,13 @@ pub async fn load_document_from_drive(
updated_at,
collaborators: Vec::new(),
version: 1,
track_changes: None,
comments: None,
footnotes: None,
endnotes: None,
styles: None,
toc: None,
track_changes_enabled: false,
}))
}
@ -553,6 +573,13 @@ pub fn create_new_document() -> Document {
updated_at: Utc::now(),
collaborators: Vec::new(),
version: 1,
track_changes: None,
comments: None,
footnotes: None,
endnotes: None,
styles: None,
toc: None,
track_changes_enabled: false,
}
}

View file

@ -1,5 +1,4 @@
use chrono::{DateTime, Duration, Utc};
use std::collections::HashMap;
pub fn format_document_list_item(
id: &str,
@ -379,8 +378,6 @@ pub fn html_to_rtf(html: &str) -> String {
rtf.push_str("{\\fonttbl{\\f0 Arial;}}\n");
rtf.push_str("\\f0\\fs24\n");
let plain = strip_html(html);
let mut result = html.to_string();
result = result.replace("<strong>", "\\b ");
result = result.replace("</strong>", "\\b0 ");

View file

@ -220,7 +220,6 @@ pub fn configure() -> Router<Arc<AppState>> {
.route("/api/docs/convert", post(document_processing::convert_document))
.route("/api/docs/fill", post(document_processing::fill_document))
.route("/api/docs/export", post(document_processing::export_document))
.route("/api/docs/import", post(document_processing::import_document))
}
pub async fn open_file(

View file

@ -4,7 +4,6 @@ use tracing::info;
use crate::security::command_guard::SafeCommand;
use super::manager::{Finding, FindingSeverity, ScanResultStatus};
const LMD_LOG_DIR: &str = "/usr/local/maldetect/logs";
const LMD_QUARANTINE_DIR: &str = "/usr/local/maldetect/quarantine";
pub async fn run_scan(path: Option<&str>) -> Result<(ScanResultStatus, Vec<Finding>, String)> {

View file

@ -5,7 +5,6 @@ use crate::security::command_guard::SafeCommand;
use super::manager::{Finding, FindingSeverity, ScanResultStatus};
const LYNIS_REPORT_PATH: &str = "/var/log/lynis-report.dat";
const LYNIS_LOG_PATH: &str = "/var/log/lynis.log";
pub async fn run_scan() -> Result<(ScanResultStatus, Vec<Finding>, String)> {
info!("Running Lynis security audit");

View file

@ -7,7 +7,6 @@ use crate::security::command_guard::SafeCommand;
use super::manager::{Finding, FindingSeverity, ScanResultStatus};
const SURICATA_EVE_LOG: &str = "/var/log/suricata/eve.json";
const SURICATA_FAST_LOG: &str = "/var/log/suricata/fast.log";
const SURICATA_RULES_DIR: &str = "/var/lib/suricata/rules";
pub async fn get_alerts() -> Result<(ScanResultStatus, Vec<Finding>, String)> {

View file

@ -1,7 +1,6 @@
use base64::Engine;
use crate::sheet::types::{CellStyle, Spreadsheet};
use rust_xlsxwriter::{Color, Format, FormatAlign, Workbook};
use std::io::Cursor;
pub fn export_to_xlsx(sheet: &Spreadsheet) -> Result<String, String> {
let mut workbook = Workbook::new();
@ -186,7 +185,7 @@ pub fn export_to_html(sheet: &Spreadsheet) -> String {
<body>
"#);
for (ws_idx, ws) in sheet.worksheets.iter().enumerate() {
for ws in &sheet.worksheets {
html.push_str(&format!("<h2>{}</h2>\n", ws.name));
html.push_str("<table>\n");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,601 @@
use crate::shared::state::AppState;
use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive};
use crate::sheet::types::{
AddExternalLinkRequest, ArrayFormula, ArrayFormulaRequest, CellData,
CreateNamedRangeRequest, DeleteArrayFormulaRequest, DeleteNamedRangeRequest, ExternalLink,
ListExternalLinksResponse, ListNamedRangesResponse, LockCellsRequest, NamedRange,
ProtectSheetRequest, RefreshExternalLinkRequest, RemoveExternalLinkRequest, SaveResponse,
UnprotectSheetRequest, UpdateNamedRangeRequest,
};
use axum::{
extract::{Query, State},
http::StatusCode,
Json,
};
use chrono::Utc;
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
use uuid::Uuid;
pub async fn handle_protect_sheet(
State(state): State<Arc<AppState>>,
Json(req): Json<ProtectSheetRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let mut protection = req.protection;
if let Some(password) = req.password {
let mut hasher = DefaultHasher::new();
password.hash(&mut hasher);
protection.password_hash = Some(format!("{:x}", hasher.finish()));
}
sheet.worksheets[req.worksheet_index].protection = Some(protection);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Sheet protected".to_string()),
}))
}
pub async fn handle_unprotect_sheet(
State(state): State<Arc<AppState>>,
Json(req): Json<UnprotectSheetRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
if let Some(protection) = &worksheet.protection {
if let Some(hash) = &protection.password_hash {
if let Some(password) = &req.password {
let mut hasher = DefaultHasher::new();
password.hash(&mut hasher);
let provided_hash = format!("{:x}", hasher.finish());
if &provided_hash != hash {
return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({ "error": "Invalid password" })),
));
}
} else {
return Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({ "error": "Password required" })),
));
}
}
}
worksheet.protection = None;
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Sheet unprotected".to_string()),
}))
}
pub async fn handle_lock_cells(
State(state): State<Arc<AppState>>,
Json(req): Json<LockCellsRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
for row in req.start_row..=req.end_row {
for col in req.start_col..=req.end_col {
let key = format!("{row},{col}");
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
value: None,
formula: None,
style: None,
format: None,
note: None,
locked: None,
has_comment: None,
array_formula_id: None,
});
cell.locked = Some(req.locked);
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some(
if req.locked {
"Cells locked"
} else {
"Cells unlocked"
}
.to_string(),
),
}))
}
pub async fn handle_add_external_link(
State(state): State<Arc<AppState>>,
Json(req): Json<AddExternalLinkRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
let link = ExternalLink {
id: Uuid::new_v4().to_string(),
source_path: req.source_path,
link_type: req.link_type,
target_sheet: req.target_sheet,
target_range: req.target_range,
status: "active".to_string(),
last_updated: Utc::now(),
};
let links = sheet.external_links.get_or_insert_with(Vec::new);
links.push(link);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("External link added".to_string()),
}))
}
pub async fn handle_refresh_external_link(
State(state): State<Arc<AppState>>,
Json(req): Json<RefreshExternalLinkRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(links) = &mut sheet.external_links {
for link in links.iter_mut() {
if link.id == req.link_id {
link.last_updated = Utc::now();
link.status = "refreshed".to_string();
}
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("External link refreshed".to_string()),
}))
}
pub async fn handle_remove_external_link(
State(state): State<Arc<AppState>>,
Json(req): Json<RemoveExternalLinkRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(links) = &mut sheet.external_links {
links.retain(|link| link.id != req.link_id);
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("External link removed".to_string()),
}))
}
pub async fn handle_list_external_links(
State(state): State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<ListExternalLinksResponse>, (StatusCode, Json<serde_json::Value>)> {
let sheet_id = params.get("sheet_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
let links = sheet.external_links.unwrap_or_default();
Ok(Json(ListExternalLinksResponse { links }))
}
pub async fn handle_array_formula(
State(state): State<Arc<AppState>>,
Json(req): Json<ArrayFormulaRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let array_formula_id = Uuid::new_v4().to_string();
let array_formula = ArrayFormula {
id: array_formula_id.clone(),
formula: req.formula.clone(),
start_row: req.start_row,
start_col: req.start_col,
end_row: req.end_row,
end_col: req.end_col,
is_dynamic: req.formula.starts_with('=') && req.formula.contains('#'),
};
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let array_formulas = worksheet.array_formulas.get_or_insert_with(Vec::new);
array_formulas.push(array_formula);
for row in req.start_row..=req.end_row {
for col in req.start_col..=req.end_col {
let key = format!("{row},{col}");
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
value: None,
formula: None,
style: None,
format: None,
note: None,
locked: None,
has_comment: None,
array_formula_id: None,
});
cell.array_formula_id = Some(array_formula_id.clone());
if row == req.start_row && col == req.start_col {
cell.formula = Some(format!("{{{}}}", req.formula));
}
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Array formula created".to_string()),
}))
}
pub async fn handle_delete_array_formula(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteArrayFormulaRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
if let Some(array_formulas) = &mut worksheet.array_formulas {
array_formulas.retain(|af| af.id != req.array_formula_id);
}
for cell in worksheet.data.values_mut() {
if cell.array_formula_id.as_ref() == Some(&req.array_formula_id) {
cell.array_formula_id = None;
cell.formula = None;
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Array formula deleted".to_string()),
}))
}
pub async fn handle_create_named_range(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateNamedRangeRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
let named_range = NamedRange {
id: Uuid::new_v4().to_string(),
name: req.name,
scope: req.scope,
worksheet_index: req.worksheet_index,
start_row: req.start_row,
start_col: req.start_col,
end_row: req.end_row,
end_col: req.end_col,
comment: req.comment,
};
let named_ranges = sheet.named_ranges.get_or_insert_with(Vec::new);
named_ranges.push(named_range);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Named range created".to_string()),
}))
}
pub async fn handle_update_named_range(
State(state): State<Arc<AppState>>,
Json(req): Json<UpdateNamedRangeRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(named_ranges) = &mut sheet.named_ranges {
for range in named_ranges.iter_mut() {
if range.id == req.range_id {
if let Some(ref name) = req.name {
range.name = name.clone();
}
if let Some(start_row) = req.start_row {
range.start_row = start_row;
}
if let Some(start_col) = req.start_col {
range.start_col = start_col;
}
if let Some(end_row) = req.end_row {
range.end_row = end_row;
}
if let Some(end_col) = req.end_col {
range.end_col = end_col;
}
if let Some(ref comment) = req.comment {
range.comment = Some(comment.clone());
}
}
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Named range updated".to_string()),
}))
}
pub async fn handle_delete_named_range(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteNamedRangeRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(named_ranges) = &mut sheet.named_ranges {
named_ranges.retain(|r| r.id != req.range_id);
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Named range deleted".to_string()),
}))
}
pub async fn handle_list_named_ranges(
State(state): State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<ListNamedRangesResponse>, (StatusCode, Json<serde_json::Value>)> {
let sheet_id = params.get("sheet_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
let ranges = sheet.named_ranges.unwrap_or_default();
Ok(Json(ListNamedRangesResponse { ranges }))
}

43
src/sheet/handlers/ai.rs Normal file
View file

@ -0,0 +1,43 @@
use crate::shared::state::AppState;
use crate::sheet::types::{SheetAiRequest, SheetAiResponse};
use axum::{extract::State, response::IntoResponse, Json};
use std::sync::Arc;
pub async fn handle_sheet_ai(
State(_state): State<Arc<AppState>>,
Json(req): Json<SheetAiRequest>,
) -> impl IntoResponse {
let command = req.command.to_lowercase();
let response = if command.contains("sum") {
"I can help you sum values. Select a range and use the SUM formula, or I've added a SUM formula below your selection."
} else if command.contains("average") || command.contains("avg") {
"I can calculate averages. Select a range and use the AVERAGE formula."
} else if command.contains("chart") {
"To create a chart, select your data range first, then choose the chart type from the Chart menu."
} else if command.contains("sort") {
"I can sort your data. Select the range you want to sort, then specify ascending or descending order."
} else if command.contains("format") || command.contains("currency") || command.contains("percent") {
"I've applied the formatting to your selected cells."
} else if command.contains("bold") || command.contains("italic") {
"I've applied the text formatting to your selected cells."
} else if command.contains("filter") {
"I've enabled filtering on your data. Use the dropdown arrows in the header row to filter."
} else if command.contains("freeze") {
"I've frozen the specified rows/columns so they stay visible when scrolling."
} else if command.contains("merge") {
"I've merged the selected cells into one."
} else if command.contains("clear") {
"I've cleared the content from the selected cells."
} else if command.contains("help") {
"I can help you with:\n• Sum/Average columns\n• Format as currency or percent\n• Bold/Italic formatting\n• Sort data\n• Create charts\n• Filter data\n• Freeze panes\n• Merge cells"
} else {
"I understand you want help with your spreadsheet. Try commands like 'sum column B', 'format as currency', 'sort ascending', or 'create a chart'."
};
Json(SheetAiResponse {
response: response.to_string(),
action: None,
data: None,
})
}

View file

@ -0,0 +1,323 @@
use crate::shared::state::AppState;
use crate::sheet::collaboration::broadcast_sheet_change;
use crate::sheet::formulas::evaluate_formula;
use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive};
use crate::sheet::types::{
CellData, CellUpdateRequest, FormatRequest, FormulaRequest, FormulaResult, FreezePanesRequest,
MergeCellsRequest, MergedCell, SaveResponse, Worksheet,
};
use axum::{extract::State, http::StatusCode, Json};
use chrono::Utc;
use std::collections::HashMap;
use std::sync::Arc;
pub async fn handle_update_cell(
State(state): State<Arc<AppState>>,
Json(req): Json<CellUpdateRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
let (value, formula) = if req.value.starts_with('=') {
let result = evaluate_formula(&req.value, worksheet);
(Some(result.value), Some(req.value.clone()))
} else {
(Some(req.value.clone()), None)
};
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
value: None,
formula: None,
style: None,
format: None,
note: None,
locked: None,
has_comment: None,
array_formula_id: None,
});
cell.value = value;
cell.formula = formula;
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
broadcast_sheet_change(
&req.sheet_id,
&user_id,
"User",
req.row,
req.col,
&req.value,
req.worksheet_index,
)
.await;
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Cell updated".to_string()),
}))
}
pub async fn handle_format_cells(
State(state): State<Arc<AppState>>,
Json(req): Json<FormatRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
for row in req.start_row..=req.end_row {
for col in req.start_col..=req.end_col {
let key = format!("{},{}", row, col);
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
value: None,
formula: None,
style: None,
format: None,
note: None,
locked: None,
has_comment: None,
array_formula_id: None,
});
cell.style = Some(req.style.clone());
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Format applied".to_string()),
}))
}
pub async fn handle_evaluate_formula(
State(state): State<Arc<AppState>>,
Json(req): Json<FormulaRequest>,
) -> Result<Json<FormulaResult>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(_) => {
return Ok(Json(evaluate_formula(
&req.formula,
&Worksheet {
name: "temp".to_string(),
data: HashMap::new(),
column_widths: None,
row_heights: None,
frozen_rows: None,
frozen_cols: None,
merged_cells: None,
filters: None,
hidden_rows: None,
validations: None,
conditional_formats: None,
charts: None,
comments: None,
protection: None,
array_formulas: None,
},
)))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let result = evaluate_formula(&req.formula, &sheet.worksheets[req.worksheet_index]);
Ok(Json(result))
}
pub async fn handle_merge_cells(
State(state): State<Arc<AppState>>,
Json(req): Json<MergeCellsRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let merged = MergedCell {
start_row: req.start_row,
start_col: req.start_col,
end_row: req.end_row,
end_col: req.end_col,
};
let merged_cells = worksheet.merged_cells.get_or_insert_with(Vec::new);
merged_cells.push(merged);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Cells merged".to_string()),
}))
}
pub async fn handle_unmerge_cells(
State(state): State<Arc<AppState>>,
Json(req): Json<MergeCellsRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
if let Some(ref mut merged_cells) = worksheet.merged_cells {
merged_cells.retain(|m| {
!(m.start_row == req.start_row
&& m.start_col == req.start_col
&& m.end_row == req.end_row
&& m.end_col == req.end_col)
});
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Cells unmerged".to_string()),
}))
}
pub async fn handle_freeze_panes(
State(state): State<Arc<AppState>>,
Json(req): Json<FreezePanesRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
worksheet.frozen_rows = Some(req.frozen_rows);
worksheet.frozen_cols = Some(req.frozen_cols);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Panes frozen".to_string()),
}))
}

358
src/sheet/handlers/crud.rs Normal file
View file

@ -0,0 +1,358 @@
use crate::shared::state::AppState;
use crate::sheet::export::{
export_to_csv, export_to_html, export_to_json, export_to_markdown, export_to_ods,
export_to_xlsx,
};
use crate::sheet::storage::{
create_new_spreadsheet, delete_sheet_from_drive, get_current_user_id, import_spreadsheet_bytes,
list_sheets_from_drive, load_sheet_by_id, load_sheet_from_drive, parse_csv_to_worksheets,
parse_excel_to_worksheets, save_sheet_to_drive,
};
use crate::sheet::types::{
ExportRequest, LoadFromDriveRequest, LoadQuery, SaveRequest, SaveResponse, SearchQuery,
ShareRequest, Spreadsheet, SpreadsheetMetadata,
};
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use chrono::Utc;
use log::error;
use std::sync::Arc;
use uuid::Uuid;
pub async fn handle_new_sheet(
State(_state): State<Arc<AppState>>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
Ok(Json(create_new_spreadsheet()))
}
pub async fn handle_list_sheets(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<SpreadsheetMetadata>>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
match list_sheets_from_drive(&state, &user_id).await {
Ok(sheets) => Ok(Json(sheets)),
Err(e) => {
error!("Failed to list sheets: {}", e);
Ok(Json(Vec::new()))
}
}
}
pub async fn handle_search_sheets(
State(state): State<Arc<AppState>>,
Query(query): Query<SearchQuery>,
) -> Result<Json<Vec<SpreadsheetMetadata>>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let sheets = match list_sheets_from_drive(&state, &user_id).await {
Ok(s) => s,
Err(_) => Vec::new(),
};
let filtered = if let Some(q) = query.q {
let q_lower = q.to_lowercase();
sheets
.into_iter()
.filter(|s| s.name.to_lowercase().contains(&q_lower))
.collect()
} else {
sheets
};
Ok(Json(filtered))
}
pub async fn handle_load_sheet(
State(state): State<Arc<AppState>>,
Query(query): Query<LoadQuery>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
match load_sheet_from_drive(&state, &user_id, &query.id).await {
Ok(sheet) => Ok(Json(sheet)),
Err(e) => Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
)),
}
}
pub async fn handle_load_from_drive(
State(state): State<Arc<AppState>>,
Json(req): Json<LoadFromDriveRequest>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
let drive = state.drive.as_ref().ok_or_else(|| {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({ "error": "Drive not available" })),
)
})?;
let result = drive
.get_object()
.bucket(&req.bucket)
.key(&req.path)
.send()
.await
.map_err(|e| {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": format!("File not found: {e}") })),
)
})?;
let bytes = result
.body
.collect()
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": format!("Failed to read file: {e}") })),
)
})?
.into_bytes();
let ext = req.path.rsplit('.').next().unwrap_or("").to_lowercase();
let file_name = req.path.rsplit('/').next().unwrap_or("Spreadsheet");
let sheet_name = file_name
.rsplit('.')
.last()
.unwrap_or("Spreadsheet")
.to_string();
let worksheets = match ext.as_str() {
"csv" | "tsv" => {
let delimiter = if ext == "tsv" { b'\t' } else { b',' };
parse_csv_to_worksheets(&bytes, delimiter, &sheet_name).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": e })),
)
})?
}
"xlsx" | "xls" | "ods" | "xlsb" | "xlsm" => {
parse_excel_to_worksheets(&bytes, &ext).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": e })),
)
})?
}
_ => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": format!("Unsupported format: .{ext}") })),
));
}
};
let user_id = get_current_user_id();
let sheet = Spreadsheet {
id: Uuid::new_v4().to_string(),
name: sheet_name,
owner_id: user_id,
worksheets,
created_at: Utc::now(),
updated_at: Utc::now(),
named_ranges: None,
external_links: None,
};
Ok(Json(sheet))
}
pub async fn handle_save_sheet(
State(state): State<Arc<AppState>>,
Json(req): Json<SaveRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let sheet_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string());
let sheet = Spreadsheet {
id: sheet_id.clone(),
name: req.name,
owner_id: user_id.clone(),
worksheets: req.worksheets,
created_at: Utc::now(),
updated_at: Utc::now(),
named_ranges: None,
external_links: None,
};
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: sheet_id,
success: true,
message: Some("Sheet saved successfully".to_string()),
}))
}
pub async fn handle_delete_sheet(
State(state): State<Arc<AppState>>,
Json(req): Json<LoadQuery>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
if let Err(e) = delete_sheet_from_drive(&state, &user_id, &req.id).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.id.unwrap_or_default(),
success: true,
message: Some("Sheet deleted".to_string()),
}))
}
pub async fn handle_get_sheet_by_id(
State(state): State<Arc<AppState>>,
Path(sheet_id): Path<String>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
match load_sheet_by_id(&state, &user_id, &sheet_id).await {
Ok(sheet) => Ok(Json(sheet)),
Err(e) => Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
)),
}
}
pub async fn handle_share_sheet(
Json(req): Json<ShareRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some(format!("Shared with {} as {}", req.email, req.permission)),
}))
}
pub async fn handle_export_sheet(
State(state): State<Arc<AppState>>,
Json(req): Json<ExportRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let sheet = match load_sheet_by_id(&state, &user_id, &req.id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
match req.format.as_str() {
"csv" => {
let csv = export_to_csv(&sheet);
Ok(([(axum::http::header::CONTENT_TYPE, "text/csv")], csv))
}
"xlsx" => {
let xlsx = export_to_xlsx(&sheet).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
)
})?;
Ok((
[(
axum::http::header::CONTENT_TYPE,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)],
xlsx,
))
}
"json" => {
let json = export_to_json(&sheet);
Ok(([(axum::http::header::CONTENT_TYPE, "application/json")], json))
}
"html" => {
let html = export_to_html(&sheet);
Ok(([(axum::http::header::CONTENT_TYPE, "text/html")], html))
}
"ods" => {
let ods = export_to_ods(&sheet).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
)
})?;
Ok((
[(
axum::http::header::CONTENT_TYPE,
"application/vnd.oasis.opendocument.spreadsheet",
)],
ods,
))
}
"md" | "markdown" => {
let md = export_to_markdown(&sheet);
Ok(([(axum::http::header::CONTENT_TYPE, "text/markdown")], md))
}
_ => Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Unsupported format" })),
)),
}
}
pub async fn handle_import_sheet(
State(state): State<Arc<AppState>>,
mut multipart: axum::extract::Multipart,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
let mut file_bytes: Option<Vec<u8>> = None;
let mut filename = "import.xlsx".to_string();
while let Ok(Some(field)) = multipart.next_field().await {
if field.name() == Some("file") {
filename = field.file_name().unwrap_or("import.xlsx").to_string();
if let Ok(bytes) = field.bytes().await {
file_bytes = Some(bytes.to_vec());
}
}
}
let bytes = file_bytes.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "No file uploaded" })),
)
})?;
let mut sheet = import_spreadsheet_bytes(&bytes, &filename).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": e })),
)
})?;
let user_id = get_current_user_id();
sheet.owner_id = user_id.clone();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(sheet))
}

View file

@ -0,0 +1,353 @@
use crate::shared::state::AppState;
use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive};
use crate::sheet::types::{
CellData, ChartConfig, ChartOptions, ChartPosition, ChartRequest, ClearFilterRequest,
ConditionalFormatRequest, ConditionalFormatRule, DeleteChartRequest, FilterConfig,
FilterRequest, SaveResponse, SortRequest,
};
use axum::{extract::State, http::StatusCode, Json};
use chrono::Utc;
use std::sync::Arc;
use uuid::Uuid;
pub async fn handle_sort_range(
State(state): State<Arc<AppState>>,
Json(req): Json<SortRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let mut rows: Vec<Vec<Option<CellData>>> = Vec::new();
for row in req.start_row..=req.end_row {
let mut row_data = Vec::new();
for col in req.start_col..=req.end_col {
let key = format!("{},{}", row, col);
row_data.push(worksheet.data.get(&key).cloned());
}
rows.push(row_data);
}
let sort_col_idx = (req.sort_col - req.start_col) as usize;
rows.sort_by(|a, b| {
let val_a = a
.get(sort_col_idx)
.and_then(|c| c.as_ref())
.and_then(|c| c.value.clone())
.unwrap_or_default();
let val_b = b
.get(sort_col_idx)
.and_then(|c| c.as_ref())
.and_then(|c| c.value.clone())
.unwrap_or_default();
let num_a = val_a.parse::<f64>().ok();
let num_b = val_b.parse::<f64>().ok();
let cmp = match (num_a, num_b) {
(Some(na), Some(nb)) => na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal),
_ => val_a.cmp(&val_b),
};
if req.ascending {
cmp
} else {
cmp.reverse()
}
});
for (row_offset, row_data) in rows.iter().enumerate() {
for (col_offset, cell) in row_data.iter().enumerate() {
let key = format!(
"{},{}",
req.start_row + row_offset as u32,
req.start_col + col_offset as u32
);
if let Some(c) = cell {
worksheet.data.insert(key, c.clone());
} else {
worksheet.data.remove(&key);
}
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Range sorted".to_string()),
}))
}
pub async fn handle_filter_data(
State(state): State<Arc<AppState>>,
Json(req): Json<FilterRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let filters = worksheet.filters.get_or_insert_with(std::collections::HashMap::new);
filters.insert(
req.col,
FilterConfig {
filter_type: req.filter_type,
values: req.values,
condition: req.condition,
value1: req.value1,
value2: req.value2,
},
);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Filter applied".to_string()),
}))
}
pub async fn handle_clear_filter(
State(state): State<Arc<AppState>>,
Json(req): Json<ClearFilterRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
if let Some(ref mut filters) = worksheet.filters {
if let Some(col) = req.col {
filters.remove(&col);
} else {
filters.clear();
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Filter cleared".to_string()),
}))
}
pub async fn handle_create_chart(
State(state): State<Arc<AppState>>,
Json(req): Json<ChartRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let chart = ChartConfig {
id: Uuid::new_v4().to_string(),
chart_type: req.chart_type,
title: req.title.unwrap_or_else(|| "Chart".to_string()),
data_range: req.data_range,
label_range: req.label_range.unwrap_or_default(),
position: req.position.unwrap_or(ChartPosition {
row: 0,
col: 5,
width: 400,
height: 300,
}),
options: ChartOptions::default(),
datasets: vec![],
labels: vec![],
};
let charts = worksheet.charts.get_or_insert_with(Vec::new);
charts.push(chart);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Chart created".to_string()),
}))
}
pub async fn handle_delete_chart(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteChartRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
if let Some(ref mut charts) = worksheet.charts {
charts.retain(|c| c.id != req.chart_id);
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Chart deleted".to_string()),
}))
}
pub async fn handle_conditional_format(
State(state): State<Arc<AppState>>,
Json(req): Json<ConditionalFormatRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let rule = ConditionalFormatRule {
id: Uuid::new_v4().to_string(),
start_row: req.start_row,
start_col: req.start_col,
end_row: req.end_row,
end_col: req.end_col,
rule_type: req.rule_type,
condition: req.condition,
style: req.style,
priority: 1,
};
let formats = worksheet.conditional_formats.get_or_insert_with(Vec::new);
formats.push(rule);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Conditional format applied".to_string()),
}))
}

32
src/sheet/handlers/mod.rs Normal file
View file

@ -0,0 +1,32 @@
pub mod advanced;
pub mod ai;
pub mod cell_ops;
pub mod crud;
pub mod data_ops;
pub mod validation;
pub use advanced::{
handle_add_external_link, handle_array_formula, handle_create_named_range,
handle_delete_array_formula, handle_delete_named_range, handle_list_external_links,
handle_list_named_ranges, handle_lock_cells, handle_protect_sheet,
handle_refresh_external_link, handle_remove_external_link, handle_unprotect_sheet,
handle_update_named_range,
};
pub use ai::handle_sheet_ai;
pub use cell_ops::{
handle_evaluate_formula, handle_format_cells, handle_freeze_panes, handle_merge_cells,
handle_unmerge_cells, handle_update_cell,
};
pub use crud::{
handle_delete_sheet, handle_export_sheet, handle_get_sheet_by_id, handle_import_sheet,
handle_list_sheets, handle_load_from_drive, handle_load_sheet, handle_new_sheet,
handle_save_sheet, handle_search_sheets, handle_share_sheet,
};
pub use data_ops::{
handle_clear_filter, handle_conditional_format, handle_create_chart, handle_delete_chart,
handle_filter_data, handle_sort_range,
};
pub use validation::{
handle_add_comment, handle_add_note, handle_data_validation, handle_delete_comment,
handle_list_comments, handle_reply_comment, handle_resolve_comment, handle_validate_cell,
};

View file

@ -0,0 +1,469 @@
use crate::shared::state::AppState;
use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive};
use crate::sheet::types::{
AddCommentRequest, AddNoteRequest, CellComment, CellData, CommentReply, CommentWithLocation,
DataValidationRequest, DeleteCommentRequest, ListCommentsRequest, ListCommentsResponse,
ReplyCommentRequest, ResolveCommentRequest, SaveResponse, ValidateCellRequest,
ValidationResult, ValidationRule,
};
use axum::{extract::State, http::StatusCode, Json};
use chrono::Utc;
use std::sync::Arc;
use uuid::Uuid;
pub async fn handle_data_validation(
State(state): State<Arc<AppState>>,
Json(req): Json<DataValidationRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let validations = worksheet
.validations
.get_or_insert_with(std::collections::HashMap::new);
for row in req.start_row..=req.end_row {
for col in req.start_col..=req.end_col {
let key = format!("{},{}", row, col);
validations.insert(
key,
ValidationRule {
validation_type: req.validation_type.clone(),
operator: req.operator.clone(),
value1: req.value1.clone(),
value2: req.value2.clone(),
allowed_values: req.allowed_values.clone(),
error_title: None,
error_message: req.error_message.clone(),
input_title: None,
input_message: None,
},
);
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Data validation applied".to_string()),
}))
}
pub async fn handle_validate_cell(
State(state): State<Arc<AppState>>,
Json(req): Json<ValidateCellRequest>,
) -> Result<Json<ValidationResult>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
if let Some(ref validations) = worksheet.validations {
if let Some(rule) = validations.get(&key) {
let result = validate_value(&req.value, rule);
return Ok(Json(result));
}
}
Ok(Json(ValidationResult {
valid: true,
error_message: None,
}))
}
fn validate_value(value: &str, rule: &ValidationRule) -> ValidationResult {
let valid = match rule.validation_type.as_str() {
"number" => value.parse::<f64>().is_ok(),
"integer" => value.parse::<i64>().is_ok(),
"list" => rule
.allowed_values
.as_ref()
.map(|v| v.contains(&value.to_string()))
.unwrap_or(true),
"date" => chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok(),
"text_length" => {
let len = value.len();
let min = rule
.value1
.as_ref()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
let max = rule
.value2
.as_ref()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(usize::MAX);
len >= min && len <= max
}
_ => true,
};
ValidationResult {
valid,
error_message: if valid {
None
} else {
rule.error_message
.clone()
.or_else(|| Some("Invalid value".to_string()))
},
}
}
pub async fn handle_add_note(
State(state): State<Arc<AppState>>,
Json(req): Json<AddNoteRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
value: None,
formula: None,
style: None,
format: None,
note: None,
locked: None,
has_comment: None,
array_formula_id: None,
});
cell.note = Some(req.note);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Note added".to_string()),
}))
}
pub async fn handle_add_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<AddCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
let comment = CellComment {
id: Uuid::new_v4().to_string(),
author_id: user_id.clone(),
author_name: "User".to_string(),
content: req.content,
created_at: Utc::now(),
updated_at: Utc::now(),
replies: vec![],
resolved: false,
};
let comments = worksheet
.comments
.get_or_insert_with(std::collections::HashMap::new);
comments.insert(key.clone(), comment);
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
value: None,
formula: None,
style: None,
format: None,
note: None,
locked: None,
has_comment: None,
array_formula_id: None,
});
cell.has_comment = Some(true);
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Comment added".to_string()),
}))
}
pub async fn handle_reply_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<ReplyCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
if let Some(comments) = &mut worksheet.comments {
if let Some(comment) = comments.get_mut(&key) {
if comment.id == req.comment_id {
let reply = CommentReply {
id: Uuid::new_v4().to_string(),
author_id: user_id.clone(),
author_name: "User".to_string(),
content: req.content,
created_at: Utc::now(),
};
comment.replies.push(reply);
comment.updated_at = Utc::now();
}
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Reply added".to_string()),
}))
}
pub async fn handle_resolve_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<ResolveCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
if let Some(comments) = &mut worksheet.comments {
if let Some(comment) = comments.get_mut(&key) {
if comment.id == req.comment_id {
comment.resolved = req.resolved;
comment.updated_at = Utc::now();
}
}
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Comment resolved".to_string()),
}))
}
pub async fn handle_delete_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &mut sheet.worksheets[req.worksheet_index];
let key = format!("{},{}", req.row, req.col);
if let Some(comments) = &mut worksheet.comments {
comments.remove(&key);
}
if let Some(cell) = worksheet.data.get_mut(&key) {
cell.has_comment = Some(false);
}
sheet.updated_at = Utc::now();
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(SaveResponse {
id: req.sheet_id,
success: true,
message: Some("Comment deleted".to_string()),
}))
}
pub async fn handle_list_comments(
State(state): State<Arc<AppState>>,
Json(req): Json<ListCommentsRequest>,
) -> Result<Json<ListCommentsResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": e })),
))
}
};
if req.worksheet_index >= sheet.worksheets.len() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
));
}
let worksheet = &sheet.worksheets[req.worksheet_index];
let mut comments_list = vec![];
if let Some(comments) = &worksheet.comments {
for (key, comment) in comments {
let parts: Vec<&str> = key.split(',').collect();
if parts.len() == 2 {
if let (Ok(row), Ok(col)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
comments_list.push(CommentWithLocation {
row,
col,
comment: comment.clone(),
});
}
}
}
}
Ok(Json(ListCommentsResponse {
comments: comments_list,
}))
}

View file

@ -130,17 +130,15 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result<Vec<u8>, String> {
if let Some(ref widths) = worksheet.column_widths {
for (col_idx, width) in widths {
let col_letter = get_col_letter(*col_idx);
if let Some(col_dim) = umya_sheet.get_column_dimension_mut(&col_letter) {
col_dim.set_width(*width as f64);
}
let col_dim = umya_sheet.get_column_dimension_mut(&col_letter);
col_dim.set_width(*width as f64);
}
}
if let Some(ref heights) = worksheet.row_heights {
for (row_idx, height) in heights {
if let Some(row_dim) = umya_sheet.get_row_dimension_mut(row_idx) {
row_dim.set_height(*height as f64);
}
let row_dim = umya_sheet.get_row_dimension_mut(row_idx);
row_dim.set_height(*height as f64);
}
}
@ -157,9 +155,10 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result<Vec<u8>, String> {
if frozen_rows > 0 {
let sheet_views = umya_sheet.get_sheet_views_mut();
if let Some(view) = sheet_views.get_sheet_view_list_mut().first_mut() {
let pane = view.get_pane_mut();
pane.set_y_split(frozen_rows as f64);
pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen);
if let Some(pane) = view.get_pane_mut() {
pane.set_vertical_split(frozen_rows as f64);
pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen);
}
}
}
}
@ -168,9 +167,10 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result<Vec<u8>, String> {
if frozen_cols > 0 {
let sheet_views = umya_sheet.get_sheet_views_mut();
if let Some(view) = sheet_views.get_sheet_view_list_mut().first_mut() {
let pane = view.get_pane_mut();
pane.set_x_split(frozen_cols as f64);
pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen);
if let Some(pane) = view.get_pane_mut() {
pane.set_horizontal_split(frozen_cols as f64);
pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen);
}
}
}
}
@ -337,16 +337,21 @@ pub fn load_xlsx_from_bytes(
let coord = c.get_coordinate();
coord.get_col_num() == &col && coord.get_row_num() == &row
})
.map(|c| c.get_text().get_value().to_string());
.and_then(|c| c.get_text().get_rich_text().map(|rt| rt.get_text().to_string()));
let cell_value = value.clone();
let has_comment = note.is_some();
data.insert(
key,
CellData {
value: if value.is_empty() { None } else { Some(value) },
value: Some(cell_value),
formula,
style,
format: None,
note,
locked: None,
has_comment: has_comment.then_some(true),
array_formula_id: None,
},
);
}
@ -356,7 +361,8 @@ pub fn load_xlsx_from_bytes(
for col in 1..=max_col {
let col_letter = get_col_letter(col);
if let Some(dim) = sheet.get_column_dimension(&col_letter) {
if let Some(width) = dim.get_width() {
let width = *dim.get_width();
if width > 0.0 {
column_widths.insert(col, width.round() as u32);
}
}
@ -364,7 +370,8 @@ pub fn load_xlsx_from_bytes(
for row in 1..=max_row {
if let Some(dim) = sheet.get_row_dimension(&row) {
if let Some(height) = dim.get_height() {
let height = *dim.get_height();
if height > 0.0 {
row_heights.insert(row, height.round() as u32);
}
}
@ -373,25 +380,28 @@ pub fn load_xlsx_from_bytes(
let merged_cells: Vec<MergedCell> = sheet.get_merge_cells()
.iter()
.filter_map(|mc| {
let range = mc.get_range().get_range();
let range = mc.get_range().to_string();
parse_merge_range(&range)
})
.collect();
let frozen_rows = sheet.get_sheet_views()
let frozen_rows = sheet.get_sheets_views()
.get_sheet_view_list()
.first()
.and_then(|v| v.get_pane().get_y_split())
.map(|y| y as u32);
.and_then(|v| v.get_pane())
.map(|p| *p.get_vertical_split() as u32)
.filter(|&v| v > 0);
let frozen_cols = sheet.get_sheet_views()
let frozen_cols = sheet.get_sheets_views()
.get_sheet_view_list()
.first()
.and_then(|v| v.get_pane().get_x_split())
.map(|x| x as u32);
.and_then(|v| v.get_pane())
.map(|p| *p.get_horizontal_split() as u32)
.filter(|&v| v > 0);
let sheet_name = sheet.get_name().to_string();
worksheets.push(Worksheet {
name: sheet.get_name().to_string(),
name: sheet_name,
data,
column_widths: if column_widths.is_empty() { None } else { Some(column_widths) },
row_heights: if row_heights.is_empty() { None } else { Some(row_heights) },
@ -403,10 +413,15 @@ pub fn load_xlsx_from_bytes(
validations: None,
conditional_formats: None,
charts: None,
comments: None,
protection: None,
array_formulas: None,
});
}
let spreadsheet = Spreadsheet {
named_ranges: None,
external_links: None,
id: Uuid::new_v4().to_string(),
name: file_name.to_string(),
owner_id: user_id.to_string(),
@ -424,58 +439,72 @@ fn extract_cell_style(cell: &umya_spreadsheet::Cell) -> Option<CellStyle> {
let fill = style.get_fill();
let alignment = style.get_alignment();
let font_weight = if font.get_bold() { Some("bold".to_string()) } else { None };
let font_style = if font.get_italic() { Some("italic".to_string()) } else { None };
let font_weight = font.as_ref().and_then(|f| if *f.get_bold() { Some("bold".to_string()) } else { None });
let font_style = font.as_ref().and_then(|f| if *f.get_italic() { Some("italic".to_string()) } else { None });
let underline = font.get_underline();
let strikethrough = font.get_strikethrough();
let text_decoration = if underline != "none" || strikethrough {
let underline_str = font.as_ref().map(|f| f.get_underline().to_string()).unwrap_or_default();
let has_strikethrough = font.as_ref().map(|f| *f.get_strikethrough()).unwrap_or(false);
let text_decoration = {
let mut dec = Vec::new();
if underline != "none" {
if underline_str != "none" && !underline_str.is_empty() {
dec.push("underline");
}
if strikethrough {
if has_strikethrough {
dec.push("line-through");
}
Some(dec.join(" "))
} else {
None
};
let font_size = Some(font.get_size().round() as u32);
let font_family = Some(font.get_name().to_string());
let color = font.get_color().get_argb().map(|c| {
let s = c.to_string();
if s.len() >= 8 {
format!("#{}", &s[2..])
if dec.is_empty() {
None
} else {
format!("#{s}")
Some(dec.join(" "))
}
});
};
let background = fill.get_pattern_fill().get_foreground_color().get_argb().map(|c| {
let s = c.to_string();
if s.len() >= 8 {
format!("#{}", &s[2..])
let font_size = font.as_ref().map(|f| f.get_size().round() as u32);
let font_family = font.as_ref().map(|f| f.get_name().to_string());
let color = font.as_ref().map(|f| {
let argb = f.get_color().get_argb();
if argb.len() == 8 {
format!("#{}", &argb[2..])
} else if argb.is_empty() {
"#000000".to_string()
} else {
format!("#{s}")
format!("#{argb}")
}
});
}).filter(|c| c != "#000000");
let text_align = match alignment.get_horizontal().to_string().as_str() {
"left" => Some("left".to_string()),
"center" => Some("center".to_string()),
"right" => Some("right".to_string()),
_ => None,
};
let background = fill.and_then(|f| f.get_pattern_fill()).and_then(|pf| {
pf.get_foreground_color().map(|color| {
let argb = color.get_argb();
if argb.len() >= 8 {
format!("#{}", &argb[2..])
} else if argb.is_empty() {
"#FFFFFF".to_string()
} else {
format!("#{argb}")
}
})
}).filter(|c| c != "#FFFFFF");
let vertical_align = match alignment.get_vertical().to_string().as_str() {
"top" => Some("top".to_string()),
"center" => Some("middle".to_string()),
"bottom" => Some("bottom".to_string()),
_ => None,
};
let text_align = alignment.map(|a| {
use umya_spreadsheet::structs::HorizontalAlignmentValues;
match a.get_horizontal() {
HorizontalAlignmentValues::Left => Some("left".to_string()),
HorizontalAlignmentValues::Center => Some("center".to_string()),
HorizontalAlignmentValues::Right => Some("right".to_string()),
_ => None,
}
}).flatten();
let vertical_align = alignment.map(|a| {
use umya_spreadsheet::structs::VerticalAlignmentValues;
match a.get_vertical() {
VerticalAlignmentValues::Top => Some("top".to_string()),
VerticalAlignmentValues::Center => Some("middle".to_string()),
VerticalAlignmentValues::Bottom => Some("bottom".to_string()),
_ => None,
}
}).flatten();
if font_weight.is_some() || font_style.is_some() || text_decoration.is_some()
|| color.is_some() || background.is_some() || text_align.is_some() {
@ -886,7 +915,7 @@ pub fn parse_ods_to_worksheets(bytes: &[u8]) -> Result<Vec<Worksheet>, String> {
let mut row_idx = 0u32;
let mut in_table = false;
let mut in_row = false;
let mut col_idx = 0u32;
let chars: Vec<char> = content.chars().collect();
@ -933,10 +962,10 @@ pub fn parse_ods_to_worksheets(bytes: &[u8]) -> Result<Vec<Worksheet>, String> {
}
in_table = false;
} else if tag.starts_with("table:table-row") && !tag.ends_with('/') {
in_row = true;
col_idx = 0;
} else if tag == "/table:table-row" {
in_row = false;
row_idx += 1;
} else if tag.starts_with("table:table-cell") {
let mut cell_value = String::new();

View file

@ -5,7 +5,6 @@ use crate::slides::storage::{
get_current_user_id, list_presentations_from_drive, load_presentation_by_id,
load_presentation_from_drive, save_presentation_to_drive,
};
use crate::slides::utils::slides_from_markdown;
use crate::slides::types::{
AddElementRequest, AddMediaRequest, AddSlideRequest, ApplyThemeRequest,
ApplyTransitionToAllRequest, CollaborationCursor, CollaborationSelection, DeleteElementRequest,

View file

@ -1,5 +1,5 @@
use crate::shared::state::AppState;
use crate::slides::ooxml::{load_pptx_preserving, update_pptx_text};
use crate::slides::ooxml::update_pptx_text;
use crate::slides::types::{
ElementContent, ElementStyle, Presentation, PresentationMetadata, Slide,
SlideBackground, SlideElement,
@ -97,11 +97,7 @@ pub async fn save_presentation_as_pptx(
let pptx_bytes = if let Some(original_bytes) = get_cached_presentation_bytes(&presentation.id).await {
let slide_texts: Vec<Vec<String>> = presentation.slides.iter().map(|slide| {
slide.elements.iter().filter_map(|el| {
if let ElementContent::Text { text, .. } = &el.content {
Some(text.clone())
} else {
None
}
el.content.text.clone()
}).collect()
}).collect();
update_pptx_text(&original_bytes, &slide_texts).unwrap_or_else(|_| {
@ -697,6 +693,8 @@ fn parse_slide_xml(xml_content: &str, slide_num: usize) -> Slide {
},
notes: None,
transition: None,
transition_config: None,
media: None,
}
}

View file

@ -2,7 +2,6 @@ use crate::slides::types::{
ElementContent, ElementStyle, Presentation, PresentationTheme, Slide, SlideBackground,
SlideElement, ThemeColors, ThemeFonts,
};
use base64::Engine;
use uuid::Uuid;
pub fn create_default_theme() -> PresentationTheme {
@ -110,6 +109,8 @@ pub fn create_title_slide(theme: &PresentationTheme) -> Slide {
},
notes: None,
transition: None,
transition_config: None,
media: None,
}
}
@ -200,6 +201,8 @@ pub fn create_content_slide(theme: &PresentationTheme) -> Slide {
},
notes: None,
transition: None,
transition_config: None,
media: None,
}
}
@ -217,6 +220,8 @@ pub fn create_blank_slide(theme: &PresentationTheme) -> Slide {
},
notes: None,
transition: None,
transition_config: None,
media: None,
}
}