mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-02 16:30:50 -06:00
132 lines
6.2 KiB
Rust
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(),
|
|
))
|
|
}
|