Fix overlapping route panic: remove duplicate /api/docs/import from drive module
This commit is contained in:
parent
9c2a4dbb97
commit
3fc3c58816
20 changed files with 2355 additions and 2181 deletions
|
|
@ -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 })),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)> {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)> {
|
||||
|
|
|
|||
|
|
@ -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
601
src/sheet/handlers/advanced.rs
Normal file
601
src/sheet/handlers/advanced.rs
Normal 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
43
src/sheet/handlers/ai.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
323
src/sheet/handlers/cell_ops.rs
Normal file
323
src/sheet/handlers/cell_ops.rs
Normal 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
358
src/sheet/handlers/crud.rs
Normal 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))
|
||||
}
|
||||
353
src/sheet/handlers/data_ops.rs
Normal file
353
src/sheet/handlers/data_ops.rs
Normal 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
32
src/sheet/handlers/mod.rs
Normal 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,
|
||||
};
|
||||
469
src/sheet/handlers/validation.rs
Normal file
469
src/sheet/handlers/validation.rs
Normal 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,
|
||||
}))
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue