ElyTable: a CSV import pushed aside by another dialog comes back with its choices (T-10)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 19:06:57 -04:00
1 parent 6145b5cea2
commit 345e248ff7
14 files changed
+73 -21

No files matched your search

+3 -3
View File
@@ -81,7 +81,7 @@ impl Book {
} }
(Asked::Refresh, Ok(_)) => book.warn_key("table.pivot.none", cx), (Asked::Refresh, Ok(_)) => book.warn_key("table.pivot.none", cx),
(_, Ok(Found::Pivot(Some(pivot)))) => { (_, Ok(Found::Pivot(Some(pivot)))) => {
book.dialog = Some(Dialog::Pivot { book.open_dialog(Dialog::Pivot {
tab: id, tab: id,
source: pivot.source, source: pivot.source,
fields: pivot.fields, fields: pivot.fields,
@@ -120,7 +120,7 @@ impl Book {
if picked.top.iter().any(String::is_empty) { if picked.top.iter().any(String::is_empty) {
return self.warn_key("table.pivot.headers", cx); return self.warn_key("table.pivot.headers", cx);
} }
self.dialog = Some(Dialog::Pivot { self.open_dialog(Dialog::Pivot {
tab: id, tab: id,
source: picked.data, source: picked.data,
fields: picked.top, fields: picked.top,
@@ -146,7 +146,7 @@ impl Book {
range: ely_office::chart::field(range, "table.chart.range", window, cx), range: ely_office::chart::field(range, "table.chart.range", window, cx),
look: ely_office::chart::LookFields::new(&look, window, cx), look: ely_office::chart::LookFields::new(&look, window, cx),
}; };
self.dialog = Some(Dialog::Chart { self.open_dialog(Dialog::Chart {
tab: id, tab: id,
chart: chart.map(Box::new), chart: chart.map(Box::new),
look: Box::new(look), look: Box::new(look),
+2 -2
View File
@@ -121,7 +121,7 @@ impl Book {
|worker, doc| doc.selection_text(worker), |worker, doc| doc.selection_text(worker),
move |book, text, _, cx| match text { move |book, text, _, cx| match text {
Ok(text) if filled(&text) > 1 => { Ok(text) if filled(&text) > 1 => {
book.dialog = Some(Dialog::Merge { tab }); book.open_dialog(Dialog::Merge { tab });
cx.notify(); cx.notify();
} }
Ok(_) => book.run_in(id, "cells.merge", &["false"], cx), Ok(_) => book.run_in(id, "cells.merge", &["false"], cx),
@@ -150,7 +150,7 @@ impl Book {
return self.toast(text, ely_gpui_component::primitives::Severity::Warning, cx); return self.toast(text, ely_gpui_component::primitives::Severity::Warning, cx);
}; };
let field = cx.new(|cx| TextInput::new(window, cx)); let field = cx.new(|cx| TextInput::new(window, cx));
self.dialog = Some(Dialog::Size { tab, column, field }); self.open_dialog(Dialog::Size { tab, column, field });
cx.notify(); cx.notify();
} }
} }
+3 -3
View File
@@ -228,7 +228,7 @@ impl Book {
fn data_dialog(&mut self, id: u64, asked: Asked, picked: Selection, state: ReadState, cx: &mut Context<Self>) { fn data_dialog(&mut self, id: u64, asked: Asked, picked: Selection, state: ReadState, cx: &mut Context<Self>) {
log::info!("table: {asked:?} on {}", picked.range); log::info!("table: {asked:?} on {}", picked.range);
let header = picked.header; let header = picked.header;
self.dialog = Some(match asked { self.open_dialog(match asked {
Asked::Sort => { Asked::Sort => {
let (first, ..) = picked.area.expect("a data area"); let (first, ..) = picked.area.expect("a data area");
Dialog::Sort { Dialog::Sort {
@@ -314,7 +314,7 @@ impl Book {
return book.warn_key("table.columns.same", cx); return book.warn_key("table.columns.same", cx);
} }
let other = cx.new(|cx| TextInput::new(window, cx)); let other = cx.new(|cx| TextInput::new(window, cx));
book.dialog = Some(Dialog::Columns { book.open_dialog(Dialog::Columns {
tab: id, tab: id,
separators: vec!["9".into()], separators: vec!["9".into()],
other, other,
@@ -394,7 +394,7 @@ impl Book {
.filter(|entry| entry.selected) .filter(|entry| entry.selected)
.map(|entry| SharedString::from(entry.text.clone())) .map(|entry| SharedString::from(entry.text.clone()))
.collect(); .collect();
book.dialog = Some(Dialog::Column { book.open_dialog(Dialog::Column {
tab: id, tab: id,
column, column,
row, row,
+2 -2
View File
@@ -116,7 +116,7 @@ impl Book {
let name = cx.new(|cx| TextInput::new(window, cx)); let name = cx.new(|cx| TextInput::new(window, cx));
let range = cx.new(|cx| TextInput::new(window, cx)); let range = cx.new(|cx| TextInput::new(window, cx));
range.update(cx, |range, cx| range.set_text(refers, cx)); range.update(cx, |range, cx| range.set_text(refers, cx));
book.dialog = Some(Dialog::Define { book.open_dialog(Dialog::Define {
tab: id, tab: id,
name, name,
range, range,
@@ -206,7 +206,7 @@ impl Book {
let range = cx.new(|cx| TextInput::new(window, cx)); let range = cx.new(|cx| TextInput::new(window, cx));
let formula = first.formula.clone(); let formula = first.formula.clone();
range.update(cx, |range, cx| range.set_text(formula, cx)); range.update(cx, |range, cx| range.set_text(formula, cx));
book.dialog = Some(Dialog::Names { book.open_dialog(Dialog::Names {
tab: id, tab: id,
names, names,
chosen: 0, chosen: 0,
+1 -1
View File
@@ -49,7 +49,7 @@ impl Book {
let Some(tab) = self.core.active else { let Some(tab) = self.core.active else {
return self.warn_key("command.blocked.no_document", cx); return self.warn_key("command.blocked.no_document", cx);
}; };
self.dialog = Some(Dialog::Export { self.open_dialog(Dialog::Export {
tab, tab,
format: Format::Pdf, format: Format::Pdf,
}); });
+1 -1
View File
@@ -143,7 +143,7 @@ impl Book {
rows: field(sheet::rows_ref(layout.rows), "table.page.rows_hint", cx), rows: field(sheet::rows_ref(layout.rows), "table.page.rows_hint", cx),
columns: field(sheet::columns_ref(layout.columns), "table.page.columns_hint", cx), columns: field(sheet::columns_ref(layout.columns), "table.page.columns_hint", cx),
}; };
self.dialog = Some(Dialog::Page { self.open_dialog(Dialog::Page {
tab: id, tab: id,
landscape: layout.landscape, landscape: layout.landscape,
scale: layout.scale, scale: layout.scale,
+1 -1
View File
@@ -58,7 +58,7 @@ impl Book {
confirm: masked("table.protect.confirm", cx), confirm: masked("table.protect.confirm", cx),
now, now,
}; };
book.dialog = Some(Dialog::Protect { book.open_dialog(Dialog::Protect {
tab: id, tab: id,
protecting: Box::new(protecting), protecting: Box::new(protecting),
}); });
+3 -3
View File
@@ -143,7 +143,7 @@ impl Book {
} else { } else {
rule rule
}; };
self.dialog = Some(Dialog::Validation { self.open_dialog(Dialog::Validation {
tab: id, tab: id,
rule: Box::new(rule), rule: Box::new(rule),
fields: Box::new(fields), fields: Box::new(fields),
@@ -275,7 +275,7 @@ impl Book {
}; };
let value1 = cx.new(|cx| TextInput::new(window, cx)); let value1 = cx.new(|cx| TextInput::new(window, cx));
let value2 = cx.new(|cx| TextInput::new(window, cx)); let value2 = cx.new(|cx| TextInput::new(window, cx));
self.dialog = Some(Dialog::Conditional { self.open_dialog(Dialog::Conditional {
tab: id, tab: id,
kind: "cell", kind: "cell",
operator: "greater", operator: "greater",
@@ -338,7 +338,7 @@ impl Book {
book.toast(text, Severity::Info, cx); book.toast(text, Severity::Info, cx);
} }
Ok(formats) => { Ok(formats) => {
book.dialog = Some(Dialog::Formats { book.open_dialog(Dialog::Formats {
tab: id, tab: id,
formats, formats,
chosen: 0, chosen: 0,
+3 -3
View File
@@ -228,7 +228,7 @@ impl Book {
return self.warn("table.show.none", cx); return self.warn("table.show.none", cx);
}; };
let tab = tab.id; let tab = tab.id;
self.dialog = Some(Dialog::Show { self.open_dialog(Dialog::Show {
tab, tab,
hidden, hidden,
chosen, chosen,
@@ -246,7 +246,7 @@ impl Book {
}; };
let field = cx.new(|cx| TextInput::new(window, cx)); let field = cx.new(|cx| TextInput::new(window, cx));
field.update(cx, |field, cx| field.set_text(name, cx)); field.update(cx, |field, cx| field.set_text(name, cx));
self.dialog = Some(Dialog::Rename { tab, at, index, field }); self.open_dialog(Dialog::Rename { tab, at, index, field });
cx.notify(); cx.notify();
} }
@@ -277,7 +277,7 @@ impl Book {
}, },
move |book, read, _, cx| match read { move |book, read, _, cx| match read {
Ok((references, at, name)) => { Ok((references, at, name)) => {
book.dialog = Some(Dialog::Delete { book.open_dialog(Dialog::Delete {
tab: id, tab: id,
at, at,
index, index,
+36
View File
@@ -377,3 +377,39 @@ fn text_files_wait_their_turn_for_the_import_dialog() {
names.sort(); names.sort();
assert_eq!(names, ["a.csv", "b.csv"], "both files open"); assert_eq!(names, ["a.csv", "b.csv"], "both files open");
} }
#[test]
fn an_import_waits_behind_a_dialog_that_displaces_it() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some((window, book)) = book(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
blank(&mut cx, &book);
let path = dir.path().join("wait.csv");
std::fs::write(&path, "Id,Ignored\n1E3,hidden\n").expect("writes");
update(&mut cx, &book, |book, cx| book.ask_import(path.clone(), cx));
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Import(pending) if !pending.reading));
update(&mut cx, &book, |book, _| {
if let Some(Dialog::Import(pending)) = &mut book.dialog {
pending.how.columns = vec![Column::Text, Column::Skip];
}
});
window
.update(&mut cx, |_, window, cx| {
book.update(cx, |book, cx| book.on_export(&ely_ui::commands::Export, window, cx))
})
.expect("the window");
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Export { .. }));
update(&mut cx, &book, |book, cx| {
book.dialog = None;
cx.notify();
});
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Import(pending) if pending.path.ends_with("wait.csv")));
cx.run_until_parked();
update(&mut cx, &book, |book, cx| book.import_asked(cx));
settle(&cx, |app| book.read(app).active_tab().is_some_and(|tab| tab.name == "wait.csv"));
// Text keeps `1E3`; the skipped column stays empty.
assert_eq!(cells(&mut cx, &book, "A2:B2").trim_end(), "1E3");
}
+13 -1
View File
@@ -253,14 +253,26 @@ impl Book {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
/// Opens `dialog`; a displaced import waits its turn again.
pub(crate) fn open_dialog(&mut self, dialog: Dialog) {
if let Some(Dialog::Import(pending)) = self.dialog.replace(dialog) {
log::info!("table: import waits behind another dialog");
self.imports.push_front(*pending);
}
}
/// The next waiting import asks, once no dialog is open. /// The next waiting import asks, once no dialog is open.
pub(crate) fn next_import(&mut self, cx: &mut Context<Self>) { pub(crate) fn next_import(&mut self, cx: &mut Context<Self>) {
if self.dialog.is_some() { if self.dialog.is_some() {
return; return;
} }
if let Some(pending) = self.imports.pop_front() { if let Some(pending) = self.imports.pop_front() {
// A finished scan keeps the reader's column choices.
let unread = pending.reading;
self.dialog = Some(Dialog::Import(Box::new(pending))); self.dialog = Some(Dialog::Import(Box::new(pending)));
self.scan_import(cx); if unread {
self.scan_import(cx);
}
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@
- [x] T-07 表头点选、行高列宽拖动是“按编号的命令”,而每次点击与拖选都推进修订,紧接着的表头操作被当作过期拒绝 — 行列号不再算作按编号的命令;R1 PASS - [x] T-07 表头点选、行高列宽拖动是“按编号的命令”,而每次点击与拖选都推进修订,紧接着的表头操作被当作过期拒绝 — 行列号不再算作按编号的命令;R1 PASS
- [x] T-08 自动筛选的列菜单对光标所在的表而不是按钮所在的表筛选、排序 — 先把光标移到按钮所在格再排序或筛选;R1 PASS - [x] T-08 自动筛选的列菜单对光标所在的表而不是按钮所在的表筛选、排序 — 先把光标移到按钮所在格再排序或筛选;R1 PASS
- [x] T-09 导入文本文件时整文件的 UTF-8 校验在界面线程跑 — 编码与分隔符的猜测随读文件在后台;R1 PASS - [x] T-09 导入文本文件时整文件的 UTF-8 校验在界面线程跑 — 编码与分隔符的猜测随读文件在后台;R1 PASS
- [ ] T-10 CSV 导入对话框开着时打开任何别的对话框,待导入的文件静默消失 - [x] T-10 CSV 导入对话框开着时打开任何别的对话框,待导入的文件静默消失 — 被挤开的导入回到队头,已读完的保留列类型;R2 PASS
- [ ] T-11 对话框比其标签活得久:导出、定义名称、重命名不可见却“开着”,挡住导入;名称表的“转到”激活已关闭的标签 - [ ] T-11 对话框比其标签活得久:导出、定义名称、重命名不可见却“开着”,挡住导入;名称表的“转到”激活已关闭的标签
- [ ] T-12 打印区域是整列或整行时页面设置无法应用 - [ ] T-12 打印区域是整列或整行时页面设置无法应用
- [ ] T-13 打印失败报成“导出失败”(非 macOS 必现) - [ ] T-13 打印失败报成“导出失败”(非 macOS 必现)
+3
View File
@@ -0,0 +1,3 @@
FAIL
apps/ely-table/src/text.rs:260(恢复路径 :271、:300);apps/ely-table/src/tests/output.rs:404 — 恢复被挤占的导入会静默覆盖用户选择的列类型,违反 PRD TABLE-12。`open_dialog` 将原 Pending 放回队头,`next_import` 再次调用 `scan_import`,扫描完成后用自动推断结果覆盖 `pending.how.columns`。具体复现:已有工作簿时打开内容为 `Id,Ignored\n1E3,hidden\n` 的 CSV,等待预览完成,将两列分别设为 Text、Skip,打开导出并取消,再确认恢复的导入。实测列类型变为 `[Standard, Standard]`,内核读出的 A2:B2 为 `1000\thidden`;直接导入的对照保留 `[Text, Skip]`,A2:B2 为 `1E3`。现有回归只检查恢复后的文件路径,遗漏列类型及实际数据。修复方向:恢复已完成扫描的 Pending 时保留列类型和预览,仅对未完成的扫描安排恢复工作;补充手动列类型经对话框挤占后保持、实际导入结果正确的回归断言。验证日志:`/tmp/ely-t10-r1-probe.log`、`/tmp/ely-t10-r1-control.log`。
+1
View File
@@ -0,0 +1 @@
PASS