From 345e248ff716b2a2e8436cc3dcfa41f03a46db7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 19:06:57 -0400 Subject: [PATCH] ElyTable: a CSV import pushed aside by another dialog comes back with its choices (T-10) --- apps/ely-table/src/analysis.rs | 6 ++--- apps/ely-table/src/cells.rs | 4 ++-- apps/ely-table/src/data.rs | 6 ++--- apps/ely-table/src/formulas.rs | 4 ++-- apps/ely-table/src/output.rs | 2 +- apps/ely-table/src/page.rs | 2 +- apps/ely-table/src/protect.rs | 2 +- apps/ely-table/src/rules.rs | 6 ++--- apps/ely-table/src/sheets.rs | 6 ++--- apps/ely-table/src/tests/output.rs | 36 ++++++++++++++++++++++++++++++ apps/ely-table/src/text.rs | 14 +++++++++++- tasks/BUGS.md | 2 +- tasks/reviews/T-10-r1.md | 3 +++ tasks/reviews/T-10-r2.md | 1 + 14 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 tasks/reviews/T-10-r1.md create mode 100644 tasks/reviews/T-10-r2.md diff --git a/apps/ely-table/src/analysis.rs b/apps/ely-table/src/analysis.rs index 2d755cd..b565506 100644 --- a/apps/ely-table/src/analysis.rs +++ b/apps/ely-table/src/analysis.rs @@ -81,7 +81,7 @@ impl Book { } (Asked::Refresh, Ok(_)) => book.warn_key("table.pivot.none", cx), (_, Ok(Found::Pivot(Some(pivot)))) => { - book.dialog = Some(Dialog::Pivot { + book.open_dialog(Dialog::Pivot { tab: id, source: pivot.source, fields: pivot.fields, @@ -120,7 +120,7 @@ impl Book { if picked.top.iter().any(String::is_empty) { return self.warn_key("table.pivot.headers", cx); } - self.dialog = Some(Dialog::Pivot { + self.open_dialog(Dialog::Pivot { tab: id, source: picked.data, fields: picked.top, @@ -146,7 +146,7 @@ impl Book { range: ely_office::chart::field(range, "table.chart.range", window, cx), look: ely_office::chart::LookFields::new(&look, window, cx), }; - self.dialog = Some(Dialog::Chart { + self.open_dialog(Dialog::Chart { tab: id, chart: chart.map(Box::new), look: Box::new(look), diff --git a/apps/ely-table/src/cells.rs b/apps/ely-table/src/cells.rs index 4379f88..6567cec 100644 --- a/apps/ely-table/src/cells.rs +++ b/apps/ely-table/src/cells.rs @@ -121,7 +121,7 @@ impl Book { |worker, doc| doc.selection_text(worker), move |book, text, _, cx| match text { Ok(text) if filled(&text) > 1 => { - book.dialog = Some(Dialog::Merge { tab }); + book.open_dialog(Dialog::Merge { tab }); cx.notify(); } 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); }; 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(); } } diff --git a/apps/ely-table/src/data.rs b/apps/ely-table/src/data.rs index 4db68a9..6e198c5 100644 --- a/apps/ely-table/src/data.rs +++ b/apps/ely-table/src/data.rs @@ -228,7 +228,7 @@ impl Book { fn data_dialog(&mut self, id: u64, asked: Asked, picked: Selection, state: ReadState, cx: &mut Context) { log::info!("table: {asked:?} on {}", picked.range); let header = picked.header; - self.dialog = Some(match asked { + self.open_dialog(match asked { Asked::Sort => { let (first, ..) = picked.area.expect("a data area"); Dialog::Sort { @@ -314,7 +314,7 @@ impl Book { return book.warn_key("table.columns.same", cx); } let other = cx.new(|cx| TextInput::new(window, cx)); - book.dialog = Some(Dialog::Columns { + book.open_dialog(Dialog::Columns { tab: id, separators: vec!["9".into()], other, @@ -394,7 +394,7 @@ impl Book { .filter(|entry| entry.selected) .map(|entry| SharedString::from(entry.text.clone())) .collect(); - book.dialog = Some(Dialog::Column { + book.open_dialog(Dialog::Column { tab: id, column, row, diff --git a/apps/ely-table/src/formulas.rs b/apps/ely-table/src/formulas.rs index bc87156..48737d8 100644 --- a/apps/ely-table/src/formulas.rs +++ b/apps/ely-table/src/formulas.rs @@ -116,7 +116,7 @@ impl Book { let name = 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)); - book.dialog = Some(Dialog::Define { + book.open_dialog(Dialog::Define { tab: id, name, range, @@ -206,7 +206,7 @@ impl Book { let range = cx.new(|cx| TextInput::new(window, cx)); let formula = first.formula.clone(); range.update(cx, |range, cx| range.set_text(formula, cx)); - book.dialog = Some(Dialog::Names { + book.open_dialog(Dialog::Names { tab: id, names, chosen: 0, diff --git a/apps/ely-table/src/output.rs b/apps/ely-table/src/output.rs index cf5dd25..10782eb 100644 --- a/apps/ely-table/src/output.rs +++ b/apps/ely-table/src/output.rs @@ -49,7 +49,7 @@ impl Book { let Some(tab) = self.core.active else { return self.warn_key("command.blocked.no_document", cx); }; - self.dialog = Some(Dialog::Export { + self.open_dialog(Dialog::Export { tab, format: Format::Pdf, }); diff --git a/apps/ely-table/src/page.rs b/apps/ely-table/src/page.rs index 3d5ce9c..92f7963 100644 --- a/apps/ely-table/src/page.rs +++ b/apps/ely-table/src/page.rs @@ -143,7 +143,7 @@ impl Book { rows: field(sheet::rows_ref(layout.rows), "table.page.rows_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, landscape: layout.landscape, scale: layout.scale, diff --git a/apps/ely-table/src/protect.rs b/apps/ely-table/src/protect.rs index 592e7e7..ac7d7d3 100644 --- a/apps/ely-table/src/protect.rs +++ b/apps/ely-table/src/protect.rs @@ -58,7 +58,7 @@ impl Book { confirm: masked("table.protect.confirm", cx), now, }; - book.dialog = Some(Dialog::Protect { + book.open_dialog(Dialog::Protect { tab: id, protecting: Box::new(protecting), }); diff --git a/apps/ely-table/src/rules.rs b/apps/ely-table/src/rules.rs index 2f213c5..8abed5a 100644 --- a/apps/ely-table/src/rules.rs +++ b/apps/ely-table/src/rules.rs @@ -143,7 +143,7 @@ impl Book { } else { rule }; - self.dialog = Some(Dialog::Validation { + self.open_dialog(Dialog::Validation { tab: id, rule: Box::new(rule), fields: Box::new(fields), @@ -275,7 +275,7 @@ impl Book { }; let value1 = 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, kind: "cell", operator: "greater", @@ -338,7 +338,7 @@ impl Book { book.toast(text, Severity::Info, cx); } Ok(formats) => { - book.dialog = Some(Dialog::Formats { + book.open_dialog(Dialog::Formats { tab: id, formats, chosen: 0, diff --git a/apps/ely-table/src/sheets.rs b/apps/ely-table/src/sheets.rs index e475da2..1130d50 100644 --- a/apps/ely-table/src/sheets.rs +++ b/apps/ely-table/src/sheets.rs @@ -228,7 +228,7 @@ impl Book { return self.warn("table.show.none", cx); }; let tab = tab.id; - self.dialog = Some(Dialog::Show { + self.open_dialog(Dialog::Show { tab, hidden, chosen, @@ -246,7 +246,7 @@ impl Book { }; let field = cx.new(|cx| TextInput::new(window, 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(); } @@ -277,7 +277,7 @@ impl Book { }, move |book, read, _, cx| match read { Ok((references, at, name)) => { - book.dialog = Some(Dialog::Delete { + book.open_dialog(Dialog::Delete { tab: id, at, index, diff --git a/apps/ely-table/src/tests/output.rs b/apps/ely-table/src/tests/output.rs index 3465892..ad07500 100644 --- a/apps/ely-table/src/tests/output.rs +++ b/apps/ely-table/src/tests/output.rs @@ -377,3 +377,39 @@ fn text_files_wait_their_turn_for_the_import_dialog() { names.sort(); 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"); +} diff --git a/apps/ely-table/src/text.rs b/apps/ely-table/src/text.rs index 2b6a08d..ba61326 100644 --- a/apps/ely-table/src/text.rs +++ b/apps/ely-table/src/text.rs @@ -253,14 +253,26 @@ impl Book { .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. pub(crate) fn next_import(&mut self, cx: &mut Context) { if self.dialog.is_some() { return; } 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.scan_import(cx); + if unread { + self.scan_import(cx); + } } } diff --git a/tasks/BUGS.md b/tasks/BUGS.md index bfca13e..69361f7 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -17,7 +17,7 @@ - [x] T-07 表头点选、行高列宽拖动是“按编号的命令”,而每次点击与拖选都推进修订,紧接着的表头操作被当作过期拒绝 — 行列号不再算作按编号的命令;R1 PASS - [x] T-08 自动筛选的列菜单对光标所在的表而不是按钮所在的表筛选、排序 — 先把光标移到按钮所在格再排序或筛选;R1 PASS - [x] T-09 导入文本文件时整文件的 UTF-8 校验在界面线程跑 — 编码与分隔符的猜测随读文件在后台;R1 PASS -- [ ] T-10 CSV 导入对话框开着时打开任何别的对话框,待导入的文件静默消失 +- [x] T-10 CSV 导入对话框开着时打开任何别的对话框,待导入的文件静默消失 — 被挤开的导入回到队头,已读完的保留列类型;R2 PASS - [ ] T-11 对话框比其标签活得久:导出、定义名称、重命名不可见却“开着”,挡住导入;名称表的“转到”激活已关闭的标签 - [ ] T-12 打印区域是整列或整行时页面设置无法应用 - [ ] T-13 打印失败报成“导出失败”(非 macOS 必现) diff --git a/tasks/reviews/T-10-r1.md b/tasks/reviews/T-10-r1.md new file mode 100644 index 0000000..290ffb1 --- /dev/null +++ b/tasks/reviews/T-10-r1.md @@ -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`。 diff --git a/tasks/reviews/T-10-r2.md b/tasks/reviews/T-10-r2.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/T-10-r2.md @@ -0,0 +1 @@ +PASS