mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 04:06:52 -06:00
Merge storytold/gridcraft#2461: PivotTables: Enable show details turns Show Details off for a PivotTable
# Conflicts: # ROADMAP.md
This commit is contained in:
@@ -139,6 +139,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
| 2026-10-24 | Status bar: a right-click opens a menu that sets which selection statistics show (Average, Count and Sum by default; Numerical Count, Minimum and Maximum are new), kept with the preferences (`view.statusBar`) |
|
||||
| 2026-10-24 | Ctrl+Shift+U (Control+Shift+U on a Mac) expands and collapses the formula bar, on the grid and while editing (`view.expandFormulaBar`); on Windows and Linux it used to underline |
|
||||
| 2026-10-24 | Fill handle: from a date, the right-drag menu and the Auto Fill Options button offer Fill Days, Fill Weekdays (Saturday and Sunday skipped), Fill Months and Fill Years |
|
||||
| 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) |
|
||||
|
||||
@@ -188,6 +188,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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",
|
||||
@@ -755,8 +764,13 @@ fn show_details(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
.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()))?;
|
||||
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."));
|
||||
}
|
||||
@@ -824,6 +838,32 @@ fn show_details(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
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 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.
|
||||
fn options(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
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<Json> {
|
||||
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<Json> {
|
||||
let d = s.doc()?;
|
||||
let name = match str_param(p, "table") {
|
||||
@@ -861,6 +901,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::<Vec<_>>(),
|
||||
"valuesOnRows": pt.values_on_rows,
|
||||
"valuesPosition": pt.values_position,
|
||||
"enableShowDetails": pt.enable_show_details,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2580,4 +2621,41 @@ 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("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("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();
|
||||
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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,6 +1044,9 @@ pub struct PivotTable {
|
||||
pub values: Vec<PivotValue>,
|
||||
pub filters: Vec<PivotFilter>,
|
||||
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,
|
||||
|
||||
@@ -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("enableShowDetails")), "{:?}", app.message);
|
||||
assert_eq!(app.session.active().unwrap().wb.sheets.len(), 1);
|
||||
}
|
||||
|
||||
@@ -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<Pivo
|
||||
pt.grand_totals_rows = x.flag("colGrandTotals", true);
|
||||
pt.grand_totals_cols = x.flag("rowGrandTotals", true);
|
||||
pt.show_headers = x.flag("showHeaders", true);
|
||||
pt.enable_show_details = x.flag("enableDrill", true);
|
||||
pt.style = x.child("pivotTableStyleInfo").and_then(|s| s.attr("name")).unwrap_or("").to_string();
|
||||
if x.child("calculatedItems").is_some() || x.path(&["filters", "filter"]).is_some() {
|
||||
cx.warn(format!("PivotTable \"{}\": calculated items and label/value filters are not supported", pt.name));
|
||||
|
||||
@@ -80,7 +80,8 @@ fn workbook() -> 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#"<pivotTableDefinition name="Drill" cacheId="1" {attr}><location ref="C1:F4"/><pivotFields><pivotField axis="axisRow"><items><item x="0"/><item x="1"/><item t="default"/></items></pivotField><pivotField dataField="1"/></pivotFields><rowFields><field x="0"/></rowFields><dataFields><dataField name="Sum of V" fld="1"/></dataFields></pivotTableDefinition>"#
|
||||
)
|
||||
};
|
||||
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#"<pivotTableDefinition name="OneValue" cacheId="1" dataOnRows="1"><location ref="C1:D4"/><pivotFields><pivotField axis="axisRow"><items><item x="0"/><item x="1"/><item t="default"/></items></pivotField><pivotField dataField="1"/></pivotFields><rowFields><field x="0"/></rowFields><dataFields><dataField name="Total" fld="1"/></dataFields></pivotTableDefinition>"#;
|
||||
|
||||
+5
-2
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user