Merge pull request #5417 from storytold/fix/sec-ui-automation-handoffs

UI: automation can't open the browser or viewer; Preview in Browser stays in its folder; Graph Data is bounded
This commit is contained in:
BFlat
2026-11-03 23:54:56 +07:00
committed by GitHub
5 changed files with 46 additions and 6 deletions
+4 -1
View File
@@ -468,7 +468,10 @@ fn typed_number(x: f64) -> Value {
/// the rows then scroll ([`super::modal::scrolled_rows`]).
pub(super) fn schema_fields(app: &mut crate::VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, m: &Manifest) -> bool {
let (changed, help) = schema_rows(ui, d, m);
if let Some(url) = help {
// The browser is another app: a click automation sent opens nothing.
if let Some(url) = help
&& app.refuse_automation("the plug-in help button").is_ok()
{
app.open_url(&url);
}
changed
+16 -3
View File
@@ -106,8 +106,11 @@ pub(crate) fn grid_from_data(v: &Value) -> Grid {
/// columns past the data.
pub(crate) fn data_from_grid(grid: &Grid) -> Value {
let filled = |s: &String| !s.trim().is_empty();
let rows = grid.iter().rposition(|r| r.iter().any(filled)).map_or(1, |i| i + 1).max(1);
let cols = grid.iter().take(rows).filter_map(|r| r.iter().rposition(filled)).max().map_or(1, |i| i + 1).max(1);
// At most a graph's rows and columns, as a file gives (`grid_from_text`): a grid sent by
// automation is any size, and the values are their product.
let grid = &grid[..grid.len().min(MAX_ROWS)];
let rows = grid.iter().rposition(|r| r.iter().take(MAX_COLS).any(filled)).map_or(1, |i| i + 1).max(1);
let cols = grid.iter().take(rows).filter_map(|r| r.iter().take(MAX_COLS).rposition(filled)).max().map_or(1, |i| i + 1).max(1);
let label = |s: &str| {
let s = s.trim();
// A label typed in straight quotes (as the CSV form takes a number as a label) is the text inside.
@@ -163,7 +166,7 @@ fn clean(s: &str) -> String {
s.chars().map(|c| if matches!(c, '\t' | '\r' | '\n') { ' ' } else { c }).take(MAX_CELL).collect()
}
fn cell(grid: &Grid, r: usize, c: usize) -> String {
fn cell(grid: &[Vec<String>], r: usize, c: usize) -> String {
grid.get(r).and_then(|row| row.get(c)).cloned().unwrap_or_default()
}
@@ -514,6 +517,16 @@ mod tests {
assert_eq!(grid_from_text(&wide)[0].len(), MAX_COLS);
}
#[test]
fn a_sheet_sent_wider_and_taller_than_a_graph_is_cut_to_one() {
// One long row and one long column: their product would be ten billion cells.
let mut grid: Grid = vec![vec!["1".to_string(); 100_000]];
grid.extend((0..100_000).map(|_| vec!["2".to_string()]));
let p = data_from_grid(&grid);
assert_eq!(p["rows"].as_array().unwrap().len(), MAX_ROWS - 1);
assert_eq!(p["series"].as_array().unwrap().len(), MAX_COLS - 1);
}
#[test]
fn transpose_switch_and_moves() {
assert_eq!(transpose(&g(&[&["", "a", "b"], &["Q1", "1", "2"]])), g(&[&["", "Q1"], &["a", "1"], &["b", "2"]]));
+9 -1
View File
@@ -175,6 +175,13 @@ fn has_warnings(r: &Value) -> bool {
r["warnings"].as_array().is_some_and(|w| !w.is_empty())
}
/// The preview page's name from the document's title `stem`: one file name in the preview folder.
/// `file_stem` gives back a title it can't take a stem of ("../.."), so one with a separator or of
/// dots only is "Untitled".
pub(super) fn preview_name(stem: &str) -> &str {
if stem.trim().is_empty() || stem.contains(['/', '\\']) || stem.chars().all(|c| c == '.') { "Untitled" } else { stem }
}
/// `file.saveForWeb.browser`: the HTML page and its images in a new temporary folder, opened in the
/// default browser (desktop). When the export has warnings, the status bar shows the first in
/// place of "Opened …".
@@ -182,9 +189,10 @@ pub(crate) fn browser_preview(app: &mut VectorcraftApp, p: &Value) -> Result<Val
if io::is_web(app) {
return Err("Preview in Browser needs the desktop app (the web downloads files instead)".into());
}
app.refuse_automation("file.saveForWeb.browser")?;
let stem = app.session.active().map(|st| vectorcraft_engine::cmd::fileio::file_stem(&st.doc.title)).ok_or("no document")?;
let dir = temp_folder()?;
let path = format!("{dir}/{}.html", if stem.trim().is_empty() { "Untitled" } else { stem.as_str() });
let path = format!("{dir}/{}.html", preview_name(&stem));
let mut q = if p.is_object() { p.clone() } else { json!({}) };
if let Some(o) = q.as_object_mut() {
o.insert("path".into(), json!(path));
@@ -317,6 +317,21 @@ fn the_web_downloads_and_has_no_browser_preview() {
assert!(app.run("file.saveForWeb.browser", json!({})).is_err());
}
#[test]
fn preview_in_browser_stays_in_its_folder_and_is_the_persons_to_open() {
for (title, name) in
[("Poster", "Poster"), ("../..", "Untitled"), ("..", "Untitled"), ("a/..", "Untitled"), ("..\\x", "Untitled"), (" ", "Untitled")]
{
assert_eq!(save_for_web::preview_name(title), name, "{title}");
}
let dir = vectorcraft_testkit::temp_dir("sfw-browser-automation");
let (mut app, log) = app(&dir, false);
app.ui.answering_control = true;
assert!(app.run("file.saveForWeb.browser", json!({"format": "jpg"})).unwrap_err().contains("opens another app"));
app.ui.answering_control = false;
assert!(!log.borrow().iter().any(|l| l.starts_with("open ")), "{:?}", log.borrow());
}
#[test]
fn preview_in_browser_opens_the_page() {
let dir = vectorcraft_testkit::temp_dir("sfw-browser");
+2 -1
View File
@@ -370,7 +370,8 @@ pub fn save_pdf(app: &mut VectorcraftApp, mode: SaveMode, mut params: Value) ->
/// Open a written file in the system viewer (not on the web, which downloads it).
pub(super) fn view_file(app: &mut VectorcraftApp, path: &str) {
if !is_web(app) {
// The viewer is another app: an export automation asked for is written but not opened.
if !is_web(app) && app.refuse_automation("viewAfterSaving").is_ok() {
app.open_url(&file_url(path));
}
}