Files
gridcraft/crates/engine/src/cmd/extra/data.rs
T

132 lines
6.2 KiB
Rust

use super::{CellRef, EngineError, Json, RangeRef, Result, Session, TableColumn, TotalsFn, Value, bad, edit, json, ok, str_param, target_range};
pub(super) fn table_resize(s: &mut Session, p: &Json) -> Result<Json> {
let r = str_param(p, "range").and_then(RangeRef::parse).ok_or_else(|| bad("table.resize", "missing `range`"))?;
let d = s.doc()?;
let si = d.wb.active_sheet();
let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
let ti = match str_param(p, "table") {
Some(n) => sh.tables.iter().position(|t| t.name.eq_ignore_ascii_case(n)),
None => sh.tables.iter().position(|t| t.range.contains(d.selection.active)),
}
.ok_or_else(|| bad("table.resize", "select a cell in a table"))?;
edit(s, |cx| {
let sheet = cx.sheet_mut(si)?;
let headers: Vec<String> = (r.start.col..=r.end.col).map(|c| sheet.value(CellRef::new(r.start.row, c)).display()).collect();
if sheet.tables.iter().enumerate().any(|(i, o)| i != ti && o.range.intersects(&r)) {
return Err(EngineError::Other("A table can't overlap another table.".into()));
}
let t = sheet.tables.get_mut(ti).ok_or_else(|| bad("table", "gone"))?;
if r.start.row != t.range.start.row || !r.intersects(&t.range) {
return Err(EngineError::Other(
"The headers must remain in the same row, and the resulting table range must overlap the original table range.".into(),
));
}
let mut cols = Vec::new();
for (i, h) in headers.iter().enumerate() {
let name = if h.is_empty() { format!("Column{}", i + 1) } else { h.clone() };
let existing = t.columns.iter().find(|c| c.name.eq_ignore_ascii_case(&name)).cloned();
cols.push(existing.unwrap_or(TableColumn {
name,
totals: TotalsFn::None,
totals_label: None,
formula: None,
unique_name: None,
xml: None,
}));
}
t.columns = cols;
t.range = r;
cx.structural = true;
Ok(Json::Null)
})
}
pub(super) fn from_csv(s: &mut Session, p: &Json) -> Result<Json> {
let bytes = if let Some(t) = str_param(p, "text") {
t.as_bytes().to_vec()
} else if let Some(path) = str_param(p, "path") {
crate::io::read_file(path)?
} else {
s.ui_requests.push(crate::UiRequest::Dialog("open".into(), json!({})));
return ok();
};
let text = String::from_utf8_lossy(&bytes);
// JSON array of objects → rows.
let rows: Vec<Vec<Json>> =
if text.trim_start().starts_with('[') {
let arr: Vec<Json> = serde_json::from_str(&text).map_err(|e| bad("data.getData", e.to_string()))?;
let mut keys: Vec<String> = Vec::new();
for o in &arr {
if let Some(m) = o.as_object() {
for k in m.keys() {
if !keys.contains(k) {
keys.push(k.clone());
}
}
}
}
let mut out = vec![keys.iter().map(|k| json!(k)).collect::<Vec<_>>()];
for o in &arr {
out.push(keys.iter().map(|k| o.get(k).cloned().unwrap_or(Json::Null)).collect());
}
out
} else {
let delim = str_param(p, "delimiter")
.and_then(|d| d.chars().next())
.unwrap_or(if text.lines().next().is_some_and(|l| l.contains('\t')) { '\t' } else { ',' });
let opts = gridcraft_xlsx::CsvOptions { delimiter: delim as u8, ..Default::default() };
let wb = gridcraft_xlsx::read_csv(&bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let sh = wb.sheet(0).ok_or(EngineError::NoDocument)?;
let Some(u) = sh.used_range() else { return Err(EngineError::Other("The file is empty.".into())) };
(u.start.row..=u.end.row)
.map(|r| {
(u.start.col..=u.end.col)
.map(|c| match sh.value(CellRef::new(r, c)) {
Value::Number(n) => json!(n),
Value::Bool(b) => json!(b),
Value::Empty => Json::Null,
v => json!(v.display()),
})
.collect()
})
.collect()
};
if rows.is_empty() || rows.len() > 1_048_576 {
return Err(EngineError::Other("Nothing to import.".into()));
}
let name = str_param(p, "sheet").map(str::to_string).or_else(|| {
str_param(p, "path").and_then(|x| std::path::Path::new(x).file_stem().and_then(std::ffi::OsStr::to_str).map(|f| f.chars().take(31).collect()))
});
let r = s.execute("home.insertSheet", json!({"name": name})).or_else(|_| s.execute("home.insertSheet", json!({})))?;
let w = rows.iter().map(Vec::len).max().unwrap_or(1).max(1);
s.execute("range.setValues", json!({"range": "A1", "values": rows}))?;
let range = RangeRef::new(CellRef::new(0, 0), CellRef::new(rows.len() as u32 - 1, w as u32 - 1));
let _ = s.execute("insert.table", json!({"range": range.a1(), "header": true}));
let _ = s.execute("home.autofitColumnWidth", json!({"cols": RangeRef::cols(0, w as u32 - 1).a1()}));
Ok(json!({"sheet": r["name"], "rows": rows.len(), "cols": w}))
}
pub(super) fn from_table_range(s: &mut Session, p: &Json) -> Result<Json> {
let r = target_range(s, p)?;
let d = s.doc()?;
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
let r = if r.is_single() { crate::selection::current_region(sh, r.start) } else { r };
let text = crate::display::range_text(&d.wb, d.wb.active_sheet(), r);
from_csv(s, &json!({"text": text, "delimiter": "\t", "sheet": format!("{} (2)", sh.name).chars().take(31).collect::<String>()}))
}
pub(super) fn queries(s: &mut Session, _: &Json) -> Result<Json> {
let d = s.doc()?;
Ok(Json::Array(
d.wb.sheets
.iter()
.flat_map(|sh| {
sh.tables
.iter()
.map(move |t| json!({"name": t.name, "sheet": sh.name, "range": t.range.a1(), "rows": t.range.height().saturating_sub(1)}))
})
.collect(),
))
}