From 30126a709abd0d79b28ac4a7783d61732f542791 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 07:37:50 -0700 Subject: [PATCH 1/2] PivotTables: Enable show details turns Show Details off for a PivotTable A PivotTable has an Enable show details option, set with the new pivot.options command ({pivot?, enableShowDetails?: bool}). While it is off, pivot.showDetails and double-clicking a value cell list no records: the double-click shows a message and does not open the cell for editing. Labels and headers still edit. Changing the option is one undo step and does not refresh the report. pivot.fields reports the setting. XLSX files keep it: an off option is written as enableDrill="0" on the pivotTableDefinition, and enableDrill is read on open (on when absent). --- ROADMAP.md | 1 + crates/engine/src/cmd/pivot.rs | 64 +++++++++++++++++++ crates/model/src/features.rs | 4 ++ crates/ui-egui/tests/it/pivot_show_details.rs | 17 ++++- crates/xlsx/src/pivot.rs | 4 ++ crates/xlsx/src/tests/pivot_tests.rs | 23 ++++++- docs/gaps.md | 7 +- 7 files changed, 116 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index da06359b5..7f3a2253e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-24 | PivotTables: Enable show details (`pivot.options` `enableShowDetails`); turned off, Show Details and double-clicking a value cell list no records, and XLSX files keep the setting (`enableDrill`) | | 2026-10-23 | Accounting negatives in text: `$(1,234)`, `$ (5)` and `(5)%` read as negative numbers when typed, coerced by arithmetic or passed to VALUE, like Excel 365; `(-5)`, `((5))`, `$5%` and dates or times in parentheses stay text | | 2026-10-23 | Calc: `INDEX(A1:C3,2)` (one index on a reference of several rows and columns) is `#REF!` as in Excel, instead of the whole row; `INDEX(A1:C3,2,)` still gives the row | | 2026-10-22 | Formula hardening: digits too many for a finite number are text, so `"1 <400 digits>/<400 digits>"` no longer coerces to NaN; a NaN column index in VLOOKUP, HLOOKUP and the D-functions is `#VALUE!` instead of an index underflow; REGEXREPLACE stops building a result as soon as it can only be too long (a worst case went from 1 GB to 7 MB) (#2163, #2162) | diff --git a/crates/engine/src/cmd/pivot.rs b/crates/engine/src/cmd/pivot.rs index 229ff22b6..5277ff98a 100644 --- a/crates/engine/src/cmd/pivot.rs +++ b/crates/engine/src/cmd/pivot.rs @@ -188,6 +188,15 @@ pub fn specs() -> Vec { has_doc, show_details ), + cmd!( + "pivot.options", + "PivotTable Options", + ["PivotTable Analyze", "PivotTable"], + None, + "{pivot?, enableShowDetails?: bool (Data › Enable show details: off, Show Details and double-clicking a value cell list no records)} → {enableShowDetails}", + has_doc, + options + ), cmd!( "pivot.delete", "Delete PivotTable", @@ -757,6 +766,9 @@ fn show_details(s: &mut Session, p: &Json) -> Result { let origin = pt.last_range.map(|r| r.start).unwrap_or_default(); let found = pv::detail_records(&d.wb, pt, active, at.row.saturating_sub(origin.row), at.col.saturating_sub(origin.col)).map_err(other)?; let (src, records) = found.ok_or_else(|| EngineError::Disabled("pivot.showDetails".into(), pv::NOT_A_VALUE_CELL.into()))?; + if !pt.enable_show_details { + return Err(other(SHOW_DETAILS_OFF)); + } if d.wb.protected_structure { return Err(other("The workbook is protected and cannot be changed.")); } @@ -824,6 +836,32 @@ fn show_details(s: &mut Session, p: &Json) -> Result { Ok(res) } +/// The message Show Details gives while Enable show details is off. +const SHOW_DETAILS_OFF: &str = "Show Details is turned off for this PivotTable. Turn on Enable show details in PivotTable Options."; + +/// PivotTable Options: Enable show details (Data). Changes only the option; the report is not +/// refreshed. Without params it returns the current setting. +fn options(s: &mut Session, p: &Json) -> Result { + let (si, pi) = find_pivot(s, p)?; + let enable = bool_param(p, "enableShowDetails"); + if p.get("enableShowDetails").is_some_and(|v| !v.is_null()) && enable.is_none() { + return Err(bad("pivot.options", "`enableShowDetails` must be true or false")); + } + let current = |s: &Session| -> Result { + let pt = s.doc()?.wb.sheet(si).and_then(|sh| sh.pivots.get(pi)).ok_or_else(|| other("There's no such PivotTable."))?; + Ok(json!({"enableShowDetails": pt.enable_show_details})) + }; + let Some(on) = enable else { return current(s) }; + if current(s)?["enableShowDetails"] == json!(on) { + return current(s); + } + edit(s, |cx| { + let pt = cx.sheet_mut(si)?.pivots.get_mut(pi).ok_or_else(|| other("There's no such PivotTable."))?; + pt.enable_show_details = on; + Ok(json!({"enableShowDetails": on})) + }) +} + fn summarize_table(s: &mut Session, p: &Json) -> Result { let d = s.doc()?; let name = match str_param(p, "table") { @@ -861,6 +899,7 @@ fn pivot_json(wb: &Workbook, si: usize, pt: &PivotTable) -> Json { "calculatedFields": pt.calculated_fields.iter().map(|c| json!({"name": c.name, "formula": c.formula})).collect::>(), "valuesOnRows": pt.values_on_rows, "valuesPosition": pt.values_position, + "enableShowDetails": pt.enable_show_details, }) } @@ -2580,4 +2619,29 @@ mod tests { } assert_eq!(s.doc().unwrap().wb.sheets.len(), 2); } + + #[test] + fn enable_show_details_off_stops_show_details() { + let mut s = sales(); + s.execute("insert.pivotTable", json!({"rows": ["Region"], "columns": ["Product"], "values": ["Sales"]})).unwrap(); + assert_eq!(s.execute("pivot.options", json!({})).unwrap()["enableShowDetails"], true); + s.execute("pivot.options", json!({"enableShowDetails": false})).unwrap(); + assert!(!pt(&s).enable_show_details); + assert_eq!(s.execute("pivot.fields", json!({})).unwrap()["enableShowDetails"], false); + let e = s.execute("pivot.showDetails", json!({"cell": "B5"})).unwrap_err().to_string(); + assert!(e.contains("Enable show details"), "{e}"); + assert_eq!(s.doc().unwrap().wb.sheets.len(), 2); + // A label still reports that it is not a value cell. + let e = s.execute("pivot.showDetails", json!({"cell": "A5"})).unwrap_err().to_string(); + assert!(!e.contains("Enable show details"), "{e}"); + // Setting the same value again adds no undo step. + let after = s.doc().unwrap().revision; + s.execute("pivot.options", json!({"enableShowDetails": false})).unwrap(); + assert_eq!(s.doc().unwrap().revision, after); + assert!(s.execute("pivot.options", json!({"enableShowDetails": "no"})).is_err()); + // One undo turns the option back on. + s.execute("edit.undo", json!({})).unwrap(); + assert!(pt(&s).enable_show_details); + assert_eq!(s.execute("pivot.showDetails", json!({"cell": "B5"})).unwrap()["records"], 2); + } } diff --git a/crates/model/src/features.rs b/crates/model/src/features.rs index aa250b9b0..e7114b802 100644 --- a/crates/model/src/features.rs +++ b/crates/model/src/features.rs @@ -1044,6 +1044,9 @@ pub struct PivotTable { pub values: Vec, pub filters: Vec, pub layout: PivotLayout, + /// PivotTable Options › Data › Enable show details: Show Details lists a value cell's + /// source records only while this is on. + pub enable_show_details: bool, /// Grand total for each row (the "Grand Total" column), like Excel's "On for Rows". pub grand_totals_rows: bool, /// Grand total for each column (the "Grand Total" row), like Excel's "On for Columns". @@ -1087,6 +1090,7 @@ impl Default for PivotTable { values: vec![], filters: vec![], layout: PivotLayout::Compact, + enable_show_details: true, grand_totals_rows: true, grand_totals_cols: true, subtotals: true, diff --git a/crates/ui-egui/tests/it/pivot_show_details.rs b/crates/ui-egui/tests/it/pivot_show_details.rs index 9f11b29eb..6300a719f 100644 --- a/crates/ui-egui/tests/it/pivot_show_details.rs +++ b/crates/ui-egui/tests/it/pivot_show_details.rs @@ -1,5 +1,6 @@ //! Double-clicking a PivotTable's value cell shows its source records on a new sheet (Excel's -//! Show Details); a label of the report still opens the cell for editing. +//! Show Details) unless its Enable show details option is off; a label of the report still opens +//! the cell for editing. use egui::{Event, Modifiers, PointerButton, Pos2, vec2}; use gridcraft_engine::Session; @@ -104,3 +105,17 @@ fn double_click_in_an_out_of_date_report_asks_for_a_refresh() { assert!(app.message.as_ref().is_some_and(|(_, m)| m.contains("out of date")), "{:?}", app.message); assert_eq!(app.session.active().unwrap().wb.sheets.len(), 1); } + +#[test] +fn double_click_with_enable_show_details_off_says_why_and_does_not_edit() { + let mut h = harness(); + h.state_mut().session.execute("pivot.options", json!({"pivot": "PivotTable1", "enableShowDetails": false})).unwrap(); + h.run_steps(2); + let pos = center(&h, "E4"); + double_click(&mut h, pos); + + let app = h.state(); + assert!(app.editor.is_none(), "the value cell must not open for editing"); + assert!(app.message.as_ref().is_some_and(|(_, m)| m.contains("Enable show details")), "{:?}", app.message); + assert_eq!(app.session.active().unwrap().wb.sheets.len(), 1); +} diff --git a/crates/xlsx/src/pivot.rs b/crates/xlsx/src/pivot.rs index f75523432..08f89335b 100644 --- a/crates/xlsx/src/pivot.rs +++ b/crates/xlsx/src/pivot.rs @@ -818,6 +818,9 @@ fn table_xml(pt: &PivotTable, idx: usize, cache_id: u32, c: &Cache, fmt_id: &dyn if !pt.show_headers { s.push_str(" showHeaders=\"0\""); } + if !pt.enable_show_details { + s.push_str(" enableDrill=\"0\""); + } // `rowGrandTotals` is the grand total row below the report (Excel's "On for Columns"), // `colGrandTotals` the grand total column at its right ("On for Rows"). if !pt.grand_totals_cols { @@ -1282,6 +1285,7 @@ fn read_table(cx: &mut Ctx<'_>, x: &El, cache: &CacheIn, id: u32) -> Option Workbook { wb.names.push(DefinedName { name: "SalesData".into(), scope: None, formula: "Data!$A$1:$F$9".into(), comment: String::new(), hidden: false }); let mut report = Sheet::new("Report"); - // Pivot 1: tabular, page filter, custom and built-in value formats, collapsed item. + // Pivot 1: tabular, page filter, custom and built-in value formats, collapsed item, Enable + // show details off. let p1 = PivotTable { id: 1, name: "Sales by Region".into(), @@ -106,6 +107,7 @@ fn workbook() -> Workbook { ], filters: vec![PivotFilter { source_col: "Channel".into(), selected: Some(vec!["Online".into()]) }], layout: PivotLayout::Tabular, + enable_show_details: false, grand_totals_rows: false, grand_totals_cols: true, subtotals: false, @@ -165,6 +167,7 @@ fn workbook() -> Workbook { PivotFilter { source_col: "Product".into(), selected: Some(vec!["A".into(), "B".into()]) }, ], layout: PivotLayout::Outline, + enable_show_details: true, grand_totals_rows: true, grand_totals_cols: false, subtotals: true, @@ -663,6 +666,24 @@ fn grand_totals_read_in_excel_direction() { } } +// `enableDrill="0"` is PivotTable Options › Data › Enable show details turned off; without the +// attribute the option is on. +#[test] +fn enable_show_details_reads_and_writes_enable_drill() { + let table = |attr: &str| { + format!( + r#""# + ) + }; + for (attr, on) in [("enableDrill=\"0\"", false), ("enableDrill=\"1\"", true), ("", true)] { + let (wb, _) = read_xlsx(&with_pivot(&table(attr), Some(GOOD_CACHE))).unwrap(); + assert_eq!(wb.sheet(0).unwrap().pivots[0].enable_show_details, on, "{attr}"); + let parts = unzip(&write_xlsx(&wb).unwrap()); + let written = String::from_utf8(parts.get("xl/pivotTables/pivotTable1.xml").unwrap().clone()).unwrap(); + assert_eq!(written.contains(" enableDrill=\"0\""), !on, "{attr}: {written}"); + } +} + #[test] fn data_on_rows_with_one_value_field_reads_without_warning() { let table = r#""#; diff --git a/docs/gaps.md b/docs/gaps.md index b07727991..761a0e10d 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (§8: PivotTable Show Details) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (§8: PivotTable Enable show details; earlier: PivotTable Show Details) · **Target:** Microsoft Excel (Microsoft 365) Every known shortfall against Excel, one entry each, ranked by user impact (what blocks someone using GridCraft for real work first). This is the work list: pick from the top, check the open @@ -85,7 +85,9 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- - **Evidence:** `parity-checklist.md` missing `insert.slicer`, `insert.timeline`, `insert.pivotChart`, `table.insertSlicer`; calculated items and GETPIVOTDATA missing (calculated fields and values on rows done). Show Details (double-click a value, subtotal or grand total cell) lists - its source records on a new sheet; the "Enable show details" option isn't modelled yet. + its source records on a new sheet. The "Enable show details" option (`pivot.options` + `enableShowDetails`, saved as `enableDrill`) turns it off; it has no PivotTable Options dialog + yet (command only), and the right-click Show Details menu item is missing. - **Impact:** pivots are the analyst's main tool. - **Estimate:** 20–30 h for beta (25–40 h full). @@ -179,6 +181,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- | Date | Change | Summary | |---|---|---| +| 2026-10-24 | minor | §8: PivotTable Enable show details: `pivot.options` `enableShowDetails` turns Show Details off for a PivotTable, read and written in XLSX as `enableDrill` | | 2026-10-23 | minor | §8: PivotTable Show Details: double-clicking a value cell lists its source records on a new sheet (#2364) | | 2026-10-20 | minor | §11: the Japanese catalog covers the dialogs, messages and menus — 943 keys (~43%), matching the pt-BR coverage; a test keeps the two catalogs in step | | 2026-10-17 | minor | §6: the five reports it tracked are closed and their PRs merged (#57, #133, #139, #140, #134; PRs #148, #155, #156, #157); the entry stays open for §15's oracle suite | From 7c10db07d9207e6a9aee60ebd2e684d744edc393 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 10:31:09 -0700 Subject: [PATCH 2/2] PivotTables: Show Details reports the turned-off option before an out-of-date report With Enable show details off, Show Details on a value cell of a report whose source has changed gave the out-of-date error. It now gives the turned-off message, and a label still reports that it is not a value cell. The message names pivot.options with enableShowDetails set to true, since there is no PivotTable Options dialog. --- crates/engine/src/cmd/pivot.rs | 26 ++++++++++++++----- crates/ui-egui/tests/it/pivot_show_details.rs | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/pivot.rs b/crates/engine/src/cmd/pivot.rs index 5277ff98a..05acb4170 100644 --- a/crates/engine/src/cmd/pivot.rs +++ b/crates/engine/src/cmd/pivot.rs @@ -764,11 +764,13 @@ fn show_details(s: &mut Session, p: &Json) -> Result { .find(|pt| pt.last_range.is_some_and(|r| r.contains(at))) .ok_or_else(|| EngineError::Disabled("pivot.showDetails".into(), pv::NOT_A_VALUE_CELL.into()))?; let origin = pt.last_range.map(|r| r.start).unwrap_or_default(); - let found = pv::detail_records(&d.wb, pt, active, at.row.saturating_sub(origin.row), at.col.saturating_sub(origin.col)).map_err(other)?; - let (src, records) = found.ok_or_else(|| EngineError::Disabled("pivot.showDetails".into(), pv::NOT_A_VALUE_CELL.into()))?; - if !pt.enable_show_details { + let found = pv::detail_records(&d.wb, pt, active, at.row.saturating_sub(origin.row), at.col.saturating_sub(origin.col)); + // With the option off, a report that is out of date or a source that cannot be read is + // reported as the option; only a cell that is not a value cell keeps its own message. + if !pt.enable_show_details && !matches!(found, Ok(None)) { return Err(other(SHOW_DETAILS_OFF)); } + let (src, records) = found.map_err(other)?.ok_or_else(|| EngineError::Disabled("pivot.showDetails".into(), pv::NOT_A_VALUE_CELL.into()))?; if d.wb.protected_structure { return Err(other("The workbook is protected and cannot be changed.")); } @@ -837,7 +839,7 @@ fn show_details(s: &mut Session, p: &Json) -> Result { } /// The message Show Details gives while Enable show details is off. -const SHOW_DETAILS_OFF: &str = "Show Details is turned off for this PivotTable. Turn on Enable show details in PivotTable Options."; +const SHOW_DETAILS_OFF: &str = "Show Details is turned off for this PivotTable. Turn it on with pivot.options and enableShowDetails set to true."; /// PivotTable Options: Enable show details (Data). Changes only the option; the report is not /// refreshed. Without params it returns the current setting. @@ -2629,11 +2631,11 @@ mod tests { assert!(!pt(&s).enable_show_details); assert_eq!(s.execute("pivot.fields", json!({})).unwrap()["enableShowDetails"], false); let e = s.execute("pivot.showDetails", json!({"cell": "B5"})).unwrap_err().to_string(); - assert!(e.contains("Enable show details"), "{e}"); + assert!(e.contains("turned off") && e.contains("pivot.options") && e.contains("enableShowDetails"), "{e}"); assert_eq!(s.doc().unwrap().wb.sheets.len(), 2); // A label still reports that it is not a value cell. let e = s.execute("pivot.showDetails", json!({"cell": "A5"})).unwrap_err().to_string(); - assert!(!e.contains("Enable show details"), "{e}"); + assert!(!e.contains("turned off"), "{e}"); // Setting the same value again adds no undo step. let after = s.doc().unwrap().revision; s.execute("pivot.options", json!({"enableShowDetails": false})).unwrap(); @@ -2644,4 +2646,16 @@ mod tests { assert!(pt(&s).enable_show_details); assert_eq!(s.execute("pivot.showDetails", json!({"cell": "B5"})).unwrap()["records"], 2); } + + #[test] + fn enable_show_details_off_is_reported_before_an_out_of_date_report() { + let mut s = sales(); + s.execute("insert.pivotTable", json!({"rows": ["Region"], "columns": ["Product"], "values": ["Sales"]})).unwrap(); + s.execute("pivot.options", json!({"enableShowDetails": false})).unwrap(); + // The source changes after the report was written, so the report is out of date. + s.execute("range.setValues", json!({"range": "Sheet1!A2", "values": [["Central"]]})).unwrap(); + let e = s.execute("pivot.showDetails", json!({"cell": "B5"})).unwrap_err().to_string(); + assert!(e.contains("turned off") && !e.contains("out of date"), "{e}"); + assert_eq!(s.doc().unwrap().wb.sheets.len(), 2); + } } diff --git a/crates/ui-egui/tests/it/pivot_show_details.rs b/crates/ui-egui/tests/it/pivot_show_details.rs index 6300a719f..959eea0f6 100644 --- a/crates/ui-egui/tests/it/pivot_show_details.rs +++ b/crates/ui-egui/tests/it/pivot_show_details.rs @@ -116,6 +116,6 @@ fn double_click_with_enable_show_details_off_says_why_and_does_not_edit() { let app = h.state(); assert!(app.editor.is_none(), "the value cell must not open for editing"); - assert!(app.message.as_ref().is_some_and(|(_, m)| m.contains("Enable show details")), "{:?}", app.message); + assert!(app.message.as_ref().is_some_and(|(_, m)| m.contains("enableShowDetails")), "{:?}", app.message); assert_eq!(app.session.active().unwrap().wb.sheets.len(), 1); }